2.31. Итераторы и генераторы

Цикл for выполняет больше работы, чем кажется. Эта страница описывает протокол итератора, на котором он работает, и ключевое слово yield, позволяющее создавать собственные итераторы.

2.31.1. Протокол итератора

Каждый объект, который можно перебрать в цикле, реализует два метода:

  • __iter__() – возвращает итератор по элементам объекта.

  • __next__() – у итератора возвращает следующий элемент или возбуждает StopIteration, когда элементов больше нет.

Встроенная функция iter() вызывает __iter__; next() вызывает __next__. Пройдём по списку вручную:

it = iter([10, 20, 30])
print(next(it))    # 10
print(next(it))    # 20
print(next(it))    # 30
print(next(it))    # raises StopIteration
Цикл for вызывает __iter__ один раз, чтобы получить итератор, затем многократно вызывает __next__, пока StopIteration не завершит цикл.

for – это синтаксический сахар для «вызвать __iter__ один раз, затем выполнять __next__ в цикле, пока не возникнет StopIteration».

Что на самом деле делает for x in items::

_it = iter(items)
while True:
    try:
        x = next(_it)
    except StopIteration:
        break
    # ... loop body ...

Каждый список, кортеж, строка, словарь, множество, файловый объект и генератор уже реализуют __iter__ и __next__ – именно поэтому все они работают с for.

2.31.2. yield и функции-генераторы

Функция, содержащая инструкцию yield, является функцией-генератором. Её вызов не запускает тело; он возвращает объект-генератор (итератор), который выполняет тело по одному yield за раз:

def count_up_to(n):
    i = 0
    while i < n:
        yield i
        i += 1

for value in count_up_to(3):
    print(value)

Вывод:

0
1
2

Каждый вызов next() возобновляет функцию до следующего yield, передаёт это значение вызывающей стороне и приостанавливается на этом месте. Локальное состояние (i в данном случае) сохраняется между возобновлениями.

Диаграмма последовательности с двумя линиями жизни (вызывающая сторона и тело генератора). Вызывающая сторона вызывает count_up_to(3), которая создаёт генератор без выполнения тела. Каждый последующий next() выполняет тело до следующего yield, возвращает выданное значение и приостанавливается. Четвёртый next() доходит до конца и возбуждает StopIteration. Переменная i сохраняется между приостановками.

next() выполняет тело до следующего yield, передаёт значение обратно и приостанавливается. Локальное состояние переживает приостановку.

Генераторы – самый простой способ лениво производить последовательность: список не строится, элементы вычисляются только тогда, когда их запрашивает потребитель, и функция может выдавать элементы бесконечно, если захочет.

2.31.3. Ленивые конвейеры

Генераторы хорошо компонуются. Вывод одного генератора может подаваться на вход другому:

def numbers():
    i = 0
    while True:
        yield i
        i += 1

def squares(source):
    for x in source:
        yield x * x

pipeline = squares(numbers())

for v in pipeline:
    if v > 100:
        break
    print(v)

Значения проходят через конвейер по одному за раз – без промежуточного списка, без встроенной верхней границы в numbers, и потребитель (for v in pipeline) решает, когда остановиться.

Три блока слева направо: numbers(), squares(source) и потребитель for-v-in-pipeline. Внизу нарисованы три цикла. В каждом цикле потребитель отправляет запрос на извлечение влево к squares, который отправляет запрос на извлечение влево к numbers; numbers выдаёт значение вправо к squares, который выдаёт своё возведённое в квадрат значение вправо потребителю.

Каждый next() у потребителя запускает одно извлечение по всей цепочке; значения существуют только тогда, когда что-то их запрашивает.

2.31.3.1. yield from

Цикл, который извлекает элементы из другого итерируемого объекта и выдаёт каждый из них, встречается достаточно часто, чтобы Python предоставил для него сокращение. Выражение yield from iter выдаёт каждое значение, которое производит итерируемый объект, по порядку – как если бы в генераторе был встроенный цикл for x in iter: yield x:

def chain(*sources):
    for source in sources:
        yield from source

for v in chain([1, 2, 3], (4, 5), "abc"):
    print(v)

Вывод:

1
2
3
4
5
a
b
c

yield from в точности эквивалентен явному циклу for, только короче, и он корректно распространяет StopIteration из внутреннего итерируемого объекта во внешний генератор – что полезно при последовательном связывании нескольких генераторов.

2.31.3.2. Когда yield исчерпан

Достижение конца функции-генератора (или явный return) автоматически возбуждает StopIteration. Нет необходимости возбуждать его вручную; окружающий цикл for видит его и завершается.

Используйте генераторы, когда производящий код естественным образом записывается как цикл с несколькими точками yield; используйте обычное списковое включение, когда вам действительно нужна вся последовательность в памяти.