Модуль 7 · Итераторы и генераторы

Справочник по Python · модуль 7 · итераторы и генераторы

Глава 7.2

7.2

Генераторы
в Python

Генератор вычисляет значения по запросу и сохраняет место остановки между вызовами next(). Его создают функцией с yield или генераторным выражением в круглых скобках.

Категория
Ленивые вычисления
Уровень
Продвинутый
Связанные темы
Итераторы, функции, включения, yield
Зачем это знать
Обрабатывать большие и потоковые данные по частям

§1Генератор — разновидность итератора

Генератор выдаёт очередное значение только тогда, когда его запрашивают. Он поддерживает iter(), next() и заканчивается через StopIteration, поэтому полностью следует протоколу итератора.

СпособСинтаксисКогда удобен
генераторная функцияdef ...: yield valueнесколько шагов, циклы и условия
генераторное выражение(expr for item in data)одно короткое преобразование

Разница с главой 7.1 в трудозатратах: там итератор требовал класса с двумя специальными методами, здесь тот же протокол получается сам собой — из обычной функции с одним новым словом.

§2Генераторная функция

Наличие yield меняет поведение функции: её вызов возвращает генератор, а значения появляются при переборе.

main.pyтри значения по запросу
def numbers():
    yield 1
    yield 2
    yield 3

generator = numbers()
print(next(generator))
print(next(generator))
print(next(generator))
Выводтри остановки yield
1
2
3

Решение принимается по тексту функции, а не во время работы: если Python видит в теле yield, вся функция становится генераторной — независимо от того, выполнится эта строка или нет.

§3Тело не выполняется при создании

Вызов генераторной функции лишь создаёт генератор. Первая строка тела выполняется при первом запросе значения.

main.pyзапуск откладывается
def demo():
    print("начало")
    yield 10

generator = demo()
print("создан")
print(next(generator))
Выводначало появляется после создан
создан
начало
10

Отсюда неочевидное следствие: проверка аргументов внутри генераторной функции сработает не в момент вызова, а при первом next() — возможно, далеко от места, где ошиблись.

§4yield возвращает значение и ставит паузу

После yield локальные переменные и текущая позиция сохраняются. Следующий next() продолжает выполнение со строки после предыдущего yield.

main.pyсостояние между остановками
def count_up(limit):
    current = 0
    while current < limit:
        yield current
        current += 1

generator = count_up(3)
print(next(generator))
print(next(generator))
print(next(generator))
Выводcurrent продолжает расти
0
1
2
Схемауправление переходит туда и обратно
время → вызывающий код генератор пауза пауза пауза next() 0 next() 1 next() 2 current = 1 current = 2 current = 3
  • генератор выполняется
  • генератор приостановлен, но жив

Из схемы видно главное: две части программы работают по очереди, а не одновременно. Пока идёт закрашенный отрезок, вызывающий код ждёт; как только генератор доходит до yield, он замирает — и в этот момент его переменная current продолжает существовать, сохраняя значение до следующего next(). Внизу схемы показано, чему она равна на каждой паузе.

Это и есть главное отличие от обычной функции. Обычная функция после return забывает всё: её локальные переменные исчезают. Приостановленный генератор продолжает существовать вместе с ними — current живёт между вызовами и помнит своё значение.

§5Обычный способ чтения — цикл for

Цикл сам запрашивает значения и останавливается в конце генератора.

main.pyквадраты до limit
def squares(limit):
    for number in range(limit):
        yield number ** 2

for value in squares(5):
    print(value)
Выводпять значений
0
1
4
9
16

Ручной next() из предыдущих параграфов нужен был, чтобы разглядеть механику. В рабочем коде генератор почти всегда читают циклом: он сам перехватывает StopIteration и не даёт ошибиться на границе.

§6Генераторное выражение

Оно похоже на списковое включение из главы 4.10, но использует круглые скобки.

main.pyзначения вычисляются при list()
squares = (number ** 2 for number in range(5))
print(list(squares))
Выводготовый список после чтения
[0, 1, 4, 9, 16]

Одна пара скобок меняет всё: [...] строит список немедленно, (...) создаёт генератор и не вычисляет ничего. Условие if в конце работает так же, как во включениях: (x for x in data if x > 0).

§7Список и генераторное выражение

СвойствоСписок [...]Генератор (...)
вычислениевсе элементы сразупо мере запросов
хранение результатоввсе элементысостояние вычисления и текущие данные
повторный переборвозможенпосле исчерпания пуст
индексitems[0]не поддерживается
длинаlen(items)неизвестна заранее

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

§8Сколько это стоит в памяти

Разницу удобно увидеть в числах — функция sys.getsizeof() показывает размер объекта в байтах.

main.pyмиллион квадратов двумя способами
import sys

squares_list = [n * n for n in range(1000000)]
squares_gen = (n * n for n in range(1000000))

print(sys.getsizeof(squares_list))
print(sys.getsizeof(squares_gen))
Выводбайты
8448728
200

Восемь мегабайт против двухсот байт — и разрыв растёт вместе с количеством элементов, потому что размер генератора от него не зависит вовсе. Заметна и разница во времени создания: список строится ощутимо дольше, генератор возникает мгновенно, ведь он ещё ничего не сосчитал.

Важная оговорка: экономия сохраняется, только пока значения обрабатывают по одному. Строка list(squares_gen) вернёт те же восемь мегабайт.

§9Генератор исчерпывается

main.pyвторой проход пуст
generator = (number for number in range(3))
print(list(generator))
print(list(generator))
Выводвозврата в начало нет
[0, 1, 2]
[]

Как и любой итератор, генератор одноразов: перемотки нет, есть только создание нового. Если данные нужны дважды, их либо сохраняют в список, либо вызывают генераторную функцию ещё раз.

§10Потенциально бесконечный генератор

Генератор может описывать последовательность без заранее известного конца. Потребитель обязан сам ограничить чтение.

main.pyберём только первые четыре
def natural_numbers():
    number = 1
    while True:
        yield number
        number += 1

generator = natural_numbers()
for _ in range(4):
    print(next(generator))
Выводгенератор мог продолжаться
1
2
3
4

Списком такое не выразить в принципе: бесконечный список построить нельзя. Здесь же while True безопасен — цикл делает ровно столько витков, сколько значений запросили.

§11Передача генератора функции

Если функция сразу перебирает значения, промежуточный список часто не нужен.

main.pyсумма квадратов без списка
result = sum(number ** 2 for number in range(10))
print(result)
Выводот 0² до 9²
285

Если генераторное выражение — единственный аргумент функции, его собственные круглые скобки можно не дублировать. Так же устроены max(), min(), any(), all() и join(): все они читают значения по одному и не нуждаются в готовом списке.

§12Построчная обработка файлов

Файловый объект уже итерируется по строкам. Генераторная функция может очищать и отбирать их, не загружая весь файл целиком.

Схемафайл передаётся открытым
def non_empty_lines(file):
    for line in file:
        cleaned = line.strip()
        if cleaned:
            yield cleaned

Открывать файл лучше через with, чтобы он гарантированно закрылся после обработки. Это самый частый рабочий сценарий генераторов: файл на несколько гигабайт обрабатывается программой, которая занимает несколько мегабайт памяти. Полную работу с файлами разберём в модуле 8, а особенности with внутри генератора — в главе 7.4.

§13return и yield

returnyield
возвращает итог обычного вызовавыдаёт очередное значение генератора
завершает функциюприостанавливает её
следующий код функции не выполняетсяследующий next() продолжает после остановки
локальные переменные исчезаютлокальные переменные сохраняются

return без значения внутри генераторной функции завершает последовательность. Возвращать значение он тоже может, но при обычном переборе оно не видно — забрать его удаётся только делегированием из главы 7.5.

§14Когда генератор не нужен

Ленивость полезна не всегда, и выбор стоит делать осознанно.

СитуацияЧто выбрать
данные читаются один раз по порядкугенератор
источник большой, бесконечный или потоковыйгенератор
результат нужен несколько разсписок
нужен доступ по индексу или len()список
десяток элементов в памятисписок — проще и понятнее
результат нужно отсортироватьсписок: сортировке нужны все элементы сразу

§15Частые ошибки

Что написаноЧто произойдётКак правильно
ожидать выполнение тела сразу после generator()вызов только создаёт объект генератораначать перебор или вызвать next()
generator[0]TypeError: генератор не поддерживает индексыnext(generator) или материализовать небольшой список
len(generator)TypeError: длина заранее неизвестнаsum(1 for _ in generator), но генератор будет исчерпан
перебирать один генератор второй разпосле исчерпания элементов не будетсоздать генератор заново
list(infinite_generator)программа будет бесконечно получать значения и расходовать памятьограничить количество элементов
заменить yield на return внутри циклафункция завершится на первом значениииспользовать yield для последовательной выдачи

§16Проверьте себя

Когда начинает выполняться тело генераторной функции?

При первом запросе значения, а не во время создания генератора.

Что сохраняет yield?

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

Когда генератор лучше списка?

Когда данные велики или поступают потоком, обрабатываются один раз по порядку и не нужны целиком одновременно.

Чем (x for x in data) отличается от [x for x in data]?

Квадратные скобки строят список сразу и целиком, круглые создают генератор, который ничего не вычисляет до запроса.

Почему генератор занимает одинаково мало памяти при любом размере данных?

Он хранит не элементы, а состояние вычисления: текущую позицию и локальные переменные функции.

Что делает функция генераторной?

Наличие yield в её теле — независимо от того, выполнится эта строка при вызове или нет.

§17Шпаргалка

Генераторная функцияdef generate(): yield value
Выражение(expr for item in data)
Следующее значениеnext(generator)
Обычное чтениеfor value in generator:
В функциюsum(x * x for x in data)
Материализоватьitems = list(generator)

Итог главы

Генератор — итератор, создаваемый функцией с yield или генераторным выражением. Он начинает вычисления при запросе, сохраняет состояние между значениями и исчерпывается после одного полного прохода. Памяти он занимает столько же независимо от объёма данных — но лишь пока их обрабатывают по одному: list() сводит эту экономию на нет. Это делает генераторы естественным выбором для больших, потоковых и даже бесконечных последовательностей и неудачным — там, где нужны индексы, длина или повторные проходы.

Глава 7.2 · Генераторы · Справочник по Python Автор: Макаров Максим Николаевич