Генераторы и yield: ленивое получение значений

Генераторная функция содержит yield и при вызове создаёт объект-генератор, не выполняя тело целиком. Каждый next продолжает вычисление до ближайшего yield, отдаёт значение и…

Генераторная функция содержит yield и при вызове создаёт объект-генератор, не выполняя тело целиком. Каждый next продолжает вычисление до ближайшего yield, отдаёт значение и замораживает локальные переменные. Следующий запрос возобновляет выполнение с сохранённого места. Это позволяет обрабатывать поток по одному элементу и не хранить все результаты одновременно.

Выполнение с паузами

return завершает обычную функцию сразу, а yield временно передаёт управление вызывающему коду. Когда генератор доходит до конца тела, он сигнализирует StopIteration. Цикл for скрывает эту механику. Сам объект является итератором: он одноразовый, сохраняет позицию и после завершения остаётся исчерпанным.

Ленивость экономит память, но откладывает работу и возможные исключения. Ошибка в середине тела проявится не при создании генератора, а только когда потребитель запросит проблемный элемент. Поэтому важно понимать, какая операция фактически запускает обход: next, for, list, sum и другие потребители.

Рабочий пример: накопительные итоги

def running_totals(values):
    total = 0
    for value in values:
        total += value
        yield total


totals = running_totals([5, -2, 10])
print(next(totals))
print(list(totals))

Ожидаемый результат:

5
[3, 13]

После первого запроса локальная переменная total сохраняет значение 5. Преобразование в список продолжает тот же объект, а не начинает расчёт заново. Чтобы получить все три итога повторно, нужно вызвать running_totals ещё раз.

Отложенные ошибки и исчерпанный генератор

Часто разработчик пишет print(running_totals(data)) и ожидает список, но видит представление вроде <generator object ...>. Функция с yield вернула ленивый итератор; для просмотра можно временно применить list, а для потоковой обработки — пройти for. Не превращайте потенциально огромный или бесконечный поток в список только ради диагностики: безопаснее запросить несколько элементов.

Если исключение появляется позже ожидаемого, поставьте вывод непосредственно перед подозрительной строкой внутри генератора и выполните next по шагам. Проверяйте входной элемент, сохранённое состояние и точку остановки. Отдельная ловушка — повторно использовать уже исчерпанный объект: пустой результат без исключения обычно означает, что генератор ранее полностью потребили.

Тренировка: ленивый фильтр строк

Создайте non_empty_lines(lines), которая удаляет пробелы по краям, пропускает пустые строки и через yield выдаёт остальные. На входе [' первая ', '', ' ', 'вторая'] должны получиться ['первая', 'вторая']. Не создавайте промежуточный список внутри функции.

Самостоятельная проверка: заведите счётчик обработанных строк и запросите только первый результат через next. Убедитесь, что строки после него ещё не обработаны. Затем дочитайте остаток циклом, повторите обход того же генератора и объясните, почему второй проход пуст.

Память, пауза и одноразовый обход

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

Генераторы при повторном чтении

Чем yield отличается от return?

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

Генератор всегда экономит память?

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

Можно ли обойти один генератор несколько раз?

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

Источники