Генераторы, yield from и ленивость

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

Генератор — функция с yield, которая возвращает значения по одному «по требованию», сохраняя своё состояние между обращениями.

yield: пауза с памятью

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

def countdown(n):
    while n > 0:
        yield n      # отдаём значение и замираем
        n -= 1

gen = countdown(3)
print(next(gen))     # 3
print(next(gen))     # 2
print("Остаток списком:", list(gen))   # дочитываем

Вывод:

3
2
Остаток списком: [1]

yield from: делегирование

yield from позволяет генератору «прокинуть» наружу все значения другого итерируемого, не выписывая цикл вручную. Удобно для склейки нескольких источников.

def chain(*iterables):
    for it in iterables:
        yield from it     # отдаём все элементы it по очереди

result = list(chain([1, 2], (3, 4), "ab"))
print(result)

Вывод:

[1, 2, 3, 4, 'a', 'b']

Без yield from пришлось бы писать вложенный цикл с yield по каждому элементу. Конструкция читается короче и яснее выражает намерение.

Генераторные выражения

Генераторное выражение выглядит как list comprehension, но в круглых скобках — и не строит список целиком, а отдаёт элементы лениво. Это экономит память на больших данных.

squares_list = [x * x for x in range(5)]   # список целиком в памяти
squares_gen = (x * x for x in range(5))    # ленивый генератор

print("Список:", squares_list)
print("Тип генератора:", type(squares_gen).__name__)
print("Сумма из генератора:", sum(squares_gen))

Вывод:

Список: [0, 1, 4, 9, 16]
Тип генератора: generator
Сумма из генератора: 30

Ленивость и бесконечность

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

def naturals():
    n = 1
    while True:        # бесконечно — но это нормально для генератора
        yield n
        n += 1

gen = naturals()
first_five = [next(gen) for _ in range(5)]
print("Первые пять:", first_five)

# берём первые квадраты, превышающие 50
result = []
for x in naturals():
    if x * x > 50:
        result.append(x * x)
    if len(result) == 3:
        break
print("Три квадрата > 50:", result)

Вывод:

Первые пять: [1, 2, 3, 4, 5]
Три квадрата > 50: [64, 81, 100]

Генератор naturals «бесконечен», но программа не зависает: мы берём элементы по требованию и останавливаемся через break. В этом сила ленивости — работа с потенциально безграничными последовательностями и большими файлами без загрузки всего в память.

Итог

  • yield отдаёт значение и замораживает состояние генератора до следующего запроса.
  • yield from делегирует выдачу другому итерируемому без ручного цикла.
  • Генераторное выражение (...) ленивое — не строит список целиком.
  • Ленивость позволяет описывать бесконечные потоки и экономить память.
Проверьте себя
1. Чем yield отличается от return?
Ayield быстрее работает
Byield отдаёт значение и замораживает состояние функции, return завершает её
Cyield можно использовать только в классах
DРазницы нет
2. Что делает конструкция yield from iterable?
AСоздаёт список из iterable
BОтдаёт наружу все элементы iterable по очереди, делегируя выдачу
CСортирует iterable
DЗапускает iterable в отдельном потоке
3. Почему генератор может описывать бесконечную последовательность, а список — нет?
AСписок ограничен 1000 элементами
BГенератор вычисляет элементы лениво, по одному, а список строит всё сразу в памяти
CБесконечных списков не существует в синтаксисе
DГенератор хранит элементы на диске
Поддержать проект