Kodokon kodokon.com

Итераторы и генераторы: yield и конвейеры

Освой протокол итерации и генераторы, чтобы обрабатывать потоки данных лениво, при постоянном расходе памяти.

10 мин · 3 вопросов

Открыть этот урок в Kodokon

for в Python - это всего лишь протокол: iter(obj) запрашивает итератор, затем вызывается next() до исключения StopIteration. Важно различать итерируемый объект (список, способный выдавать свежий итератор на каждом проходе) и итератор (курсор, который можно пройти только один раз). Понять этот протокол - значит понять, почему некоторые объекты «расходуются».

PYTHON
numbers = [10, 20, 30]
iterator = iter(numbers)
print(next(iterator))
print(next(iterator))
print(next(iterator))
# next(iterator) would raise StopIteration
Что делает цикл for, но вручную

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

PYTHON
def squares(limit):
    n = 0
    while n < limit:
        yield n * n
        n += 1

gen = squares(4)
print(next(gen))  # 0
print(list(gen))  # [1, 4, 9]
yield приостанавливает функцию между двумя значениями

Главная выгода на практике - конвейеры. Каждая ступень потребляет поток предыдущей и лениво производит свой; ничего не выполняется до финального потребления (list(), sum(), цикл for). Компромисс, о котором стоит помнить: список можно пройти несколько раз и нарезать срезами, генератор - нет; зато генератор обработает файл на 10 ГБ, не забив оперативную память. Генераторные выражения - (f(x) for x in xs if cond) - покрывают простые ступени без объявления функции.

PYTHON
lines = ["  12 ", "x", " 7", "", "30"]

def clean(rows):
    for row in rows:
        row = row.strip()
        if row.isdigit():
            yield int(row)

def keep_from(values, floor):
    return (v for v in values if v >= floor)

pipeline = keep_from(clean(lines), 10)
print(list(pipeline))  # [12, 30]
Ленивый конвейер: ничего не выполняется до list()

Проверка знаний

Убедись, что запомнил ключевые моменты этого урока.

  1. Что ты получаешь, когда вызываешь функцию, в теле которой есть yield?
    • Результат первого yield
    • Генератор, тело при этом не выполняется
    • Список всех произведённых значений
  2. Какое решающее преимущество даёт конвейер генераторов по сравнению с промежуточными списками?
    • Результаты сортируются автоматически
    • Память остаётся постоянной: значения производятся по запросу
    • Обработка распараллеливается по нескольким ядрам
  3. После первого list(gen) что вернёт второй list(gen) на том же генераторе?
    • Тот же список, что и первый вызов
    • Пустой список
    • Будет проброшено исключение