← Итератор · 🏠 Домой · Comprehensions →
Коротко. Генератор — это самый удобный способ написать
итератор: функция с yield вместо класса с __iter__/__next__.
Генератор является итератором, а не альтернативой ему.
Функция становится генератором, если в её теле есть хотя бы один yield. При
вызове она не выполняется, а возвращает объект-генератор. Тело начинает
работать только при первом next() и приостанавливается на каждом yield,
сохраняя все локальные переменные и место остановки.
def gen():
print("старт")
yield 1
print("между")
yield 2
g = gen() # ничего не напечатано — тело ещё не выполнялось
next(g) # печатает «старт», возвращает 1
next(g) # печатает «между», возвращает 2
next(g) # StopIterationПодвох. «Генератор ленивее итератора?» Нет, ленивы оба одинаково. Разница только в способе записи: то, что классом занимает 8 строк, генератором — 3.
Глубже. «Приостановка» — это сохранение кадра выполнения: локальных переменных, указателя на текущую инструкцию, стека вычислений и состояния обработки исключений. Поэтому генератор дороже по памяти, чем простой счётчик, но несравнимо дешевле, чем материализованный список.
Коротко. Экономия памяти: список хранит все элементы сразу, генератор — только текущее состояние. На больших последовательностях разница на три порядка.
import sys
sys.getsizeof([i for i in range(10_000)]) # 85176 байт
sys.getsizeof((i for i in range(10_000))) # 200 байтДва синтаксиса — генераторное выражение (в круглых скобках) и функция с yield:
gen = (i * i for i in range(10)) # для простых случаев
def gen(): # когда нужна логика
for i in range(10):
yield i * iПодвох. Список можно обойти сколько угодно раз, генератор — один. Если результат нужен дважды, генератор придётся либо материализовать в список, либо создать заново.
Глубже. Выигрыш не только в памяти, но и в задержке до первого элемента:
генератор отдаёт первое значение сразу, а список сначала посчитает все.
Это важно в конвейерах обработки и при раннем выходе (any(), next(), break).
Коротко. Делегирование другому генератору: yield from sub() пробрасывает
наружу все его значения, а заодно корректно передаёт внутрь send()/throw() и
забирает его return.
def inner():
yield 1
yield 2
return "результат"
def outer():
got = yield from inner() # got получит то, что inner вернул через return
print(got) # результат
yield 3
list(outer()) # [1, 2, 3]Без yield from пришлось бы писать for x in inner(): yield x — и это не
передавало бы ни send(), ни возвращаемое значение.
Глубже. return внутри генератора не отдаёт значение в поток итерации — он
завершает генератор, помещая значение в атрибут value исключения
StopIteration. Достать его можно либо через yield from, либо перехватив
StopIteration вручную.
Коротко. Это канал связи в обратную сторону: send() передаёт значение
внутрь генератора, throw() возбуждает в нём исключение, close() завершает его.
stateDiagram-v2
[*] --> Created : вызов gen() — тело ещё не выполнялось
Created --> Suspended : next() — «прогрев» до первого yield
Suspended --> Running : next() или send(value)
Running --> Suspended : дошли до следующего yield
Running --> Closed : return или исчерпание
Suspended --> Closed : close() возбуждает GeneratorExit,<br/>отрабатывает finally
Suspended --> Running : throw(exc) — исключение в точке остановки
Closed --> [*] : дальнейший next() даёт StopIteration
send(value) делает то же, что next(), но выражение yield возвращает
переданное значение вместо None:
def echo():
while True:
got = yield
print("получено:", got)
e = echo()
next(e) # обязательный «прогрев» до первого yield
e.send("привет") # получено: приветthrow() возбуждает исключение в точке остановки — генератор может его поймать:
def catcher():
try:
yield 1
except ValueError:
yield "поймал ValueError"
c = catcher()
next(c)
c.throw(ValueError) # 'поймал ValueError'close() возбуждает GeneratorExit, что позволяет освободить ресурсы:
def res():
try:
yield 1
yield 2
finally:
print("finally выполнен")
g = res()
next(g)
g.close() # печатает «finally выполнен»Подвох. У throw() была форма из трёх аргументов —
throw(type, value, traceback). Она устарела с python 3.12 и выдаёт
DeprecationWarning. Актуально — throw(value).
Глубже. Если генератор не исчерпан и не закрыт явно, close() вызовется при
сборке мусора — но момент этот не гарантирован. Поэтому генератор, который
держит файл или соединение, оборачивают в contextlib.closing или пишут так,
чтобы finally отрабатывал.
Коротко. Он превратится в RuntimeError. Это защита из PEP 479: раньше
случайный StopIteration молча обрывал итерацию, и баг было почти невозможно
найти.
def gen():
yield 1
raise StopIteration("oops")
list(gen())
# RuntimeError: generator raised StopIterationПодвох. Ловушка срабатывает не только на явный raise, но и на вызов
next() внутри генератора: если вложенный итератор кончится, его
StopIteration тоже станет RuntimeError. Правильно — ловить его явно или
использовать next(it, default).