Генераторы в Python
Генератор — это самый удобный способ создать итератор в Python. Вместо того
чтобы писать класс с __iter__ и __next__, вы пишете обычную функцию,
заменяя return на yield. Python сам сделает из неё итератор.
Генераторы — это про ленивые вычисления: значения не считаются заранее и не лежат в памяти все сразу. Они производятся по одному, когда их запрашивают. Поэтому генератор может обрабатывать миллиарды элементов, занимая в памяти ровно столько, сколько нужно для одного.
Самый простой генератор
def countdown(n):
while n > 0:
yield n
n -= 1
for x in countdown(3):
print(x)
Вывод:
3
2
1
Заметьте: при первом вызове countdown(3) тело функции не выполняется —
возвращается объект-генератор. Код запускается только когда мы начинаем
просить значения через next() или for.
Как работает yield
yield — это «пауза с возвращением значения». Когда исполнение доходит до
yield, оно:
Отдаёт значение наружу.
Замораживает состояние функции (локальные переменные, точку выполнения).
Ждёт следующего
next().На следующем
next()продолжает работу с того же места.
gen = countdown(3)
next(gen) --> входим в функцию, идём до yield, отдаём 3, замираем
next(gen) --> продолжаем, n=2, отдаём 2, замираем
next(gen) --> продолжаем, n=1, отдаём 1, замираем
next(gen) --> цикл завершился, выходим из функции
--> StopIteration
Зачем нужны генераторы
Самое заметное преимущество — экономия памяти:
# Список — все 10 миллионов чисел в памяти сразу
squares_list = [x*x for x in range(10_000_000)] # ~400 МБ
# Генератор — ни одного числа в памяти сверх текущего
squares_gen = (x*x for x in range(10_000_000)) # ~200 байт
Второе — ленивость: данные считаются только если понадобились. Можно описать бесконечную последовательность:
def naturals():
n = 1
while True:
yield n
n += 1
from itertools import islice
print(list(islice(naturals(), 5))) # [1, 2, 3, 4, 5]
Генераторные выражения
Краткая форма — как list comprehension, но в круглых скобках:
squares = (x*x for x in range(10))
print(sum(squares)) # 285
Часто их даже не присваивают, а сразу передают в функцию:
total = sum(x*x for x in range(10)) # скобки можно опустить
has_big = any(x > 100 for x in numbers)
names = ', '.join(user['name'] for user in users)
Практический пример: чтение большого файла
Допустим, нам нужно посчитать сумму чисел из файла с миллионом строк, и каждая строка — это число.
def read_numbers(path):
with open(path, encoding='utf-8') as f:
for line in f:
line = line.strip()
if line:
yield int(line)
total = sum(read_numbers('numbers.txt'))
print(total)
Файл читается потоково — в памяти одновременно живёт только одно число и одна текущая строка. То же самое через список заняло бы сотни мегабайт.
Практический пример: пайплайн обработки
Генераторы можно складывать в цепочку — каждый этап получает данные от предыдущего и отдаёт дальше. Это похоже на Unix-пайпы.
def read_lines(path):
with open(path, encoding='utf-8') as f:
for line in f:
yield line.rstrip()
def only_errors(lines):
for line in lines:
if 'ERROR' in line:
yield line
def extract_codes(lines):
for line in lines:
parts = line.split()
if len(parts) >= 3:
yield parts[2]
# Собираем пайплайн
lines = read_lines('app.log')
errors = only_errors(lines)
codes = extract_codes(errors)
for code in codes:
print(code)
Ни на одном этапе данные не накапливаются — обработка идёт по одной строке.
yield from
Когда генератор делегирует работу другому итерируемому объекту, используют
yield from. Это короче и быстрее, чем ручной цикл:
def flatten(nested):
for sublist in nested:
yield from sublist
print(list(flatten([[1, 2], [3, 4], [5]])))
# [1, 2, 3, 4, 5]
Двусторонняя связь: send()
Генератор может не только выдавать, но и принимать значения через
generator.send(value). Тогда yield становится выражением, возвращающим
переданное значение.
def echo():
while True:
received = yield
print('Получено:', received)
gen = echo()
next(gen) # обязательно "прокрутить" до первого yield
gen.send('hello') # Получено: hello
gen.send(42) # Получено: 42
На этой механике построены сопрограммы и (исторически) asyncio.
Генератор vs функция со списком
функция со списком: функция-генератор:
----------------- -----------------
def f(n): def g(n):
result = [] for i in range(n):
for i in range(n): yield i*i
result.append(i*i)
return result
f(5) -> [0, 1, 4, 9, 16] g(5) -> <generator object>
list(g(5)) -> [0, 1, 4, 9, 16]
готовый список сразу значения по требованию
занимает память почти не занимает память
Подводные камни
Предупреждение
Генератор одноразовый. Прошли по нему — он пуст. Чтобы пройти ещё раз — создайте новый.
``return`` в генераторе не возвращает значение, а возбуждает
StopIteration. Можно использовать как «ранний выход».Не закрытые файлы. Если генератор открыл файл и его перестали использовать на середине, файл не закроется сразу. Используйте
withвнутри генератора илиcontextlib.closing.``list(gen)`` сводит на нет ленивость. Используйте только если правда нужен весь список.
Смотрите также
Итераторы в Python — итераторы, теория
Функции в Python — функции
Python: цикл for — цикл for
Примечание
Лицензия и источники
Часть материала адаптирована из официальной документации Python (https://docs.python.org/3/tutorial/classes.html#generators), доступной под Python Software Foundation License Version 2 (PSF License). Адаптация, переработка, оригинальные примеры и пояснения — © AlashEd Wiki.