Генераторы, 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делегирует выдачу другому итерируемому без ручного цикла.- Генераторное выражение
(...)ленивое — не строит список целиком. - Ленивость позволяет описывать бесконечные потоки и экономить память.