Генераторы, yield и yield from
Генератор — функция, которая умеет приостанавливаться
Если в функции есть yield, вызов не выполняет её тело, а возвращает объект-генератор. Каждый next() выполняет код до следующего yield, отдаёт значение и замораживает функцию вместе со всеми локальными переменными:
def countdown(n):
print(" старт")
while n > 0:
print(f" отдаю {n}")
yield n # пауза: значение наружу, состояние сохранено
n -= 1
print(" конец")
g = countdown(3)
print("генератор создан:", g)
print(next(g))
print(next(g))
print(list(g)) # добирает остаток
Пройдите пример 👣 Пошагово: видно, как выполнение «прыгает» между функцией и вызывающим кодом, а n сохраняется между вызовами.
Генератор — это итератор: у него есть __iter__ и __next__, а в конце он бросает StopIteration. Значит, его можно использовать в for, list(), sum(), распаковке.
Сравните: итератор-класс и генератор
class SquaresIter: # 12 строк
def __init__(self, n):
self.i, self.n = 0, n
def __iter__(self):
return self
def __next__(self):
if self.i >= self.n:
raise StopIteration
self.i += 1
return self.i ** 2
def squares_gen(n): # 3 строки, то же поведение
for i in range(1, n + 1):
yield i ** 2
print(list(SquaresIter(5)), list(squares_gen(5)))
Генератор — самый простой способ написать итератор. В том числе метод __iter__ контейнера:
class Tree:
def __init__(self, value, *children):
self.value, self.children = value, children
def __iter__(self): # обход в глубину генератором
yield self.value
for child in self.children:
yield from child
t = Tree(1, Tree(2, Tree(4)), Tree(3))
print(list(t), sum(t), max(t))
Генераторные выражения
Как списковое включение, но в круглых скобках — и ленивое:
import sys
nums = range(1_000_000)
as_list = [x * x for x in nums]
as_gen = (x * x for x in nums)
print(sys.getsizeof(as_list), "байт у списка")
print(sys.getsizeof(as_gen), "байт у генератора")
print(sum(x * x for x in nums)) # скобки функции можно не дублировать
print(any(x > 10 for x in [1, 50, 3])) # any остановится на первом True
Когда что
Нужно один раз пройти и что-то посчитать (sum, max, any, "".join) — генераторное выражение. Нужно обращаться по индексу, проходить несколько раз, узнать len — список.
Конвейеры
Генераторы соединяются в цепочку, и данные текут по ней по одному элементу. Так можно обработать файл в гигабайты, держа в памяти одну строку:
log = """INFO старт
ERROR диск заполнен
INFO запрос /
ERROR таймаут базы
ERROR диск заполнен"""
def read_lines(text):
for line in text.splitlines():
print(f" читаю: {line}")
yield line
def only_errors(lines):
for line in lines:
if line.startswith("ERROR"):
yield line
def messages(lines):
for line in lines:
yield line.split(" ", 1)[1]
pipeline = messages(only_errors(read_lines(log)))
print("первая ошибка:", next(pipeline))
from collections import Counter
print(Counter(pipeline))
Бесконечные генераторы
from itertools import islice
def naturals(start=1):
while True: # никогда не кончается — и это нормально
yield start
start += 1
def take(n, iterable):
return list(islice(iterable, n))
evens = (n for n in naturals() if n % 2 == 0)
print(take(5, evens))
print(take(3, evens)) # продолжает с того же места
yield from
yield from iterable отдаёт все элементы другого итерируемого объекта — и правильно пробрасывает send, throw, close и возвращаемое значение подгенератора:
def letters():
yield "a"
yield "b"
return "буквы готовы" # return в генераторе → StopIteration.value
def digits():
yield from range(3)
def combined():
result = yield from letters() # результат return подгенератора
print(f" letters вернул: {result!r}")
yield from digits()
print(list(combined()))
send, throw, close
Генератор умеет не только отдавать, но и принимать значения: yield — это выражение, его результат — то, что передали в send():
def running_average():
total, count = 0, 0
average = None
while True:
value = yield average # отдаём среднее, ждём новое число
total += value
count += 1
average = total / count
avg = running_average()
next(avg) # «заводим» генератор до первого yield
print(avg.send(10), avg.send(20), avg.send(60))
avg.close() # внутри бросается GeneratorExit
На этой механике когда-то были построены корутины asyncio (до async/await). Сегодня send в прикладном коде встречается редко, но о нём спрашивают на собеседованиях.
Генератор и ресурсы
finally в генераторе выполнится, даже если генератор не дочитали — при close() или сборке мусора:
def read_records():
print(" открываю ресурс")
try:
for i in range(100):
yield i
finally:
print(" закрываю ресурс")
g = read_records()
print(next(g), next(g))
g.close()
Частые ошибки
# 1. Генератор одноразовый
gen = (x for x in [1, 2, 3])
print(sum(gen), sum(gen)) # 6 0
# 2. Функция с yield не выполняется при вызове
def save_all(items):
for item in items:
print("сохраняю", item)
yield item
save_all([1, 2]) # ничего не напечатает! генератор создан и выброшен
print("^ ничего не сохранено")
# 3. Позднее связывание в генераторном выражении
data = [1, 2, 3]
g = (x for x in data if x in data)
data = [2, 3] # первый for вычислился сразу, условие — лениво
print(list(g))
Вопрос на собеседовании
«Что такое генератор и чем он полезен?» — Функция с yield, которая возвращает итератор и выполняется лениво, приостанавливаясь на каждом yield с сохранением локального состояния. Польза: экономия памяти (данные не хранятся целиком), бесконечные последовательности, конвейеры обработки, простое написание итераторов.
Итог
yieldпревращает функцию в фабрику генераторов; тело выполняется поnext()до следующегоyield.- Генератор — итератор: ленивый и одноразовый.
- Генераторные выражения
(...)экономят память для однократного обхода. yield fromделегирует другому итерируемому объекту и получает егоreturn.sendпередаёт значение внутрь,closeзавершает генератор,finallyосвобождает ресурсы.
Практика решено 0 из 5
Напишите генератор chunks(items, size), который выдаёт списки по size элементов
из последовательности items; последний кусок может быть короче.
Какие тесты проверяют код (3)
assert list(chunks([1, 2, 3, 4, 5], 2)) == [[1, 2], [3, 4], [5]] assert list(chunks([], 3)) == [] import types; assert isinstance(chunks([1], 1), types.GeneratorType), "chunks должна быть генератором (yield)"
def chunks(items, size):
for i in range(0, len(items), size):
yield list(items[i:i + size])
Напишите бесконечный генератор primes(), выдающий простые числа 2, 3, 5, 7, 11, …
Проверка возьмёт из него первые элементы через itertools.islice.
Какие тесты проверяют код (3)
import types; assert isinstance(primes(), types.GeneratorType), "primes должна быть генератором" from itertools import islice; assert list(islice(primes(), 10)) == [2, 3, 5, 7, 11, 13, 17, 19, 23, 29] from itertools import islice; assert list(islice(primes(), 1000))[-1] == 7919
def primes():
found = []
n = 2
while True:
if all(n % p for p in found if p * p <= n):
found.append(n)
yield n
n += 1
Напишите генератор flatten(nested), который «расплющивает» произвольно вложенные списки:
flatten([1, [2, [3, [4]], 5]]) → 1, 2, 3, 4, 5. Используйте рекурсию и yield from.
Какие тесты проверяют код (2)
assert list(flatten([1, [2, [3, [4]], 5]])) == [1, 2, 3, 4, 5] assert list(flatten([[], [[]], 7])) == [7]
def flatten(nested):
for item in nested:
if isinstance(item, list):
yield from flatten(item)
else:
yield item
Что выведет код?
def gen():
print("старт")
yield 1
print("середина")
yield 2
g = gen()
print("создан")
print(next(g))
Чем (x * 2 for x in data) отличается от [x * 2 for x in data]?
Войдите, чтобы отмечать пройденные уроки.