Menu
Русский

Генераторы в Python: yield, ленивая итерация и generator-выражения

Как генераторы лениво производят значения в Python — ключевое слово yield, generator-выражения и когда они бьют обычный список.

Функция, которая делает паузы

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

Самый простой:

main.py
Output
Click Run to see the output here.

Обрати внимание на yield вместо return. Когда for впервые просит значение, Python выполняет тело до yield 1. Функция замирает ровно там, отдаёт 1 циклу и помнит, где остановилась, — переменные и всё. Следующая итерация подхватывает с того же места: current += 1, обратно к while, yield 2. И так до тех пор, пока условие не станет ложным, — тогда генератор просто останавливается.

Этот «пауза и продолжение» — и есть весь трюк.

Почему не просто построить список?

Потому что версия со списком выделяет все значения заранее:

main.py
Output
Click Run to see the output here.

Для пяти элементов нормально. Теперь представь, что нужно 50 миллионов целых, а интересен только первый, подходящий под условие. Версия со списком выделит 50 миллионов int-ов, и большую часть ты выбросишь. Версия с генератором создаёт ровно столько, сколько потребляет вызывающий. Когда for находит нужное и делает break, генератор просто останавливается.

Паттерн стоит усвоить: генераторы позволяют писать код итерации, не решая заранее, сколько результата тебе понадобится.

Generator-выражения

Если ты писал списковое включение, синтаксис ты уже знаешь — замени квадратные скобки на круглые:

main.py
Output
Click Run to see the output here.

squares_gen пока ничего не вычислил. Это просто рецепт. Итерация запускает рецепт пошагово.

Generator-выражения идеальны как аргументы функций, потребляющих итерируемое:

main.py
Output
Click Run to see the output here.

Никаких промежуточных списков. sum, max и any читают значения по одному — именно то, что им и надо.

Чтение большого файла построчно

Канонический реальный кейс для генераторов — обработать файл, который не помещается в память:

def parse_log_lines(path):
    with open(path) as f:
        for line in f:
            if line.startswith("ERROR"):
                yield line.rstrip()

for error in parse_log_lines("app.log"):
    print(error)

Файл читается лениво. Каждый вызов к генератору вытаскивает одну строку с диска, фильтрует и выдаёт. Расход памяти остаётся плоским независимо от размера файла.

Один раз и всё

У генератора один проход. После того как проитерировался до конца, он исчерпан:

main.py
Output
Click Run to see the output here.

Второй цикл не распечатает ничего. Генератору нечего отдать.

Если нужна итерация больше одного раза — либо вызови функцию-генератор заново, чтобы получить свежий, либо материализуй последовательность через list(...) и итерируй список повторно. Выбирай по цене: пересоздать дёшево, если работа дешёвая; список нормален, если последовательность маленькая.

next() и ручная итерация

for не обязателен. next() вытаскивает одно значение за раз:

main.py
Output
Click Run to see the output here.

StopIteration — это как генератор сигнализирует «я закончил». Циклы for его тихо ловят. В ручном коде можно передать значение по умолчанию — next(gen, default), — чтобы избежать исключения.

Бесконечные генераторы

Поскольку значения производятся по требованию, генератор может представлять последовательность без конца — пока потребитель не перестанет просить:

main.py
Output
Click Run to see the output here.

while True с yield внутри не подвешивает программу — это означает «если кто-то продолжает просить, продолжай выдавать». Потребитель решает, когда останавливаться.

Такой паттерн всплывает в стриминге данных, event-loop-ах и везде, где значения тянут из источника без заранее известной длины.

yield from: делегирование другой итерируемой

Если генератор хочет выдавать каждое значение из другой итерируемой, yield from делает это в одну строку:

main.py
Output
Click Run to see the output here.

Без yield from пришлось бы писать вложенный for с yield x внутри. Он также правильно пробрасывает вызовы send() и throw(), если их используешь, — но для повседневного кода считай это «выдай каждое значение из этой штуки».

Когда тянуться к генератору

Три сигнала того, что генератор — правильный инструмент:

  1. Последовательность большая, возможно бесконечная или дорогая для производства целиком.
  2. Потребитель может остановиться до конца (например, break по первому совпадению).
  3. Ты хочешь цепочку преобразований — фильтр, map, take — без построения промежуточных списков.

А когда не надо:

  • Нужен случайный доступ (seq[42]). Генераторы идут только вперёд.
  • Нужно пройти ту же последовательность несколько раз. Используй список.
  • Последовательность маленькая и уже у тебя. Списковое включение проще.

Генераторы, списковые включения и обычные списки — каждый правильный ответ для разных задач. Навык — выбирать, не сильно задумываясь. Самый быстрый способ развить интуицию — замечать для каждой итерации, которую пишешь, что лучше: «сначала произвести всё» или «производить по одному».

Дальше: контекстные менеджеры подробнее

Ты уже видел большинство идиом итерации Python. Контекстные менеджеры — оператор with — следующие, и они хорошо сочетаются с генераторами для стриминга данных из файлов и сетевых соединений.

Часто задаваемые вопросы

Что такое генератор в Python?

Генератор — это функция, производящая значения по одному и делающая паузы между ними. Пишется def, как обычная функция, но вместо returnyield. Вызов возвращает объект-генератор; каждая итерация for или каждый next() запускает тело до следующего yield.

В чём разница между списком и генератором?

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

Можно ли итерироваться по генератору дважды?

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

Учитесь программировать с Coddy

НАЧАТЬ