Skip to content

Latest commit

 

History

History
196 lines (146 loc) · 8.61 KB

File metadata and controls

196 lines (146 loc) · 8.61 KB

Генератор

← Итератор · 🏠 Домой · 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?

Коротко. Делегирование другому генератору: 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()?

Коротко. Это канал связи в обратную сторону: 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
Loading

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 отрабатывал.


Что будет, если внутри генератора возбудить StopIteration?

Коротко. Он превратится в RuntimeError. Это защита из PEP 479: раньше случайный StopIteration молча обрывал итерацию, и баг было почти невозможно найти.

def gen():
    yield 1
    raise StopIteration("oops")

list(gen())
# RuntimeError: generator raised StopIteration

Подвох. Ловушка срабатывает не только на явный raise, но и на вызов next() внутри генератора: если вложенный итератор кончится, его StopIteration тоже станет RuntimeError. Правильно — ловить его явно или использовать next(it, default).


← Итератор · 🏠 Домой · Comprehensions →