Генераторы Python и ключевое слово yield
Изучите генераторы Python и ключевое слово yield с примерами: функции-генераторы, выражения, send() и практические паттерны.
Генератор — это особый вид итератора, который производит значения по одному, по мере необходимости, вместо того чтобы вычислять их все заранее. Генераторы определяются с помощью обычного синтаксиса функций с yield вместо return. Это идиоматическое решение Python для работы с большими или бесконечными последовательностями, когда построение полного списка было бы расточительным по памяти или времени.
В этой главе рассматриваются ключевое слово yield, генераторные функции в сравнении со списками, генераторные выражения, передача значений в генератор, объединение генераторов и практические паттерны.
Что такое генератор?
Когда Python вызывает обычную функцию, он выполняет тело до конца и возвращает одно значение. Когда Python вызывает генераторную функцию, он не запускает тело вовсе — он возвращает объект-генератор. Каждый раз, когда вы вызываете next() для этого объекта, выполнение возобновляется с того места, где оно последний раз приостановилось (оператор yield), продолжается до следующего yield и снова приостанавливается.
def count_up(start, stop):
while start <= stop:
yield start # pause here, emit the value
start += 1
gen = count_up(1, 3)
print(next(gen)) # 1
print(next(gen)) # 2
print(next(gen)) # 3
# next(gen) would now raise StopIterationКлючевые механизмы:
- Тело функции не выполняется до первого вызова
next(). - Локальные переменные и указатель инструкции сохраняются между вызовами.
- Когда тело функции заканчивается (или встречает голый
return), Python автоматически вызываетStopIteration. - Цикл
forвызываетnext()за вас и корректно останавливается наStopIteration.
Ключевое слово yield
yield — единственный синтаксис, который отличает генераторную функцию от обычной. Вы можете использовать yield везде, где мог бы появиться return, в том числе внутри циклов, условных выражений и блоков try/except.
yield против return
return | yield | |
|---|---|---|
| Тип функции | Обычная | Генераторная |
| Выполнение после вызова | Выполняется до конца | Приостанавливается на yield |
| Состояние между вызовами | Сбрасывается | Сохраняется |
| Несколько значений | Одно (или кортеж) | По одному на каждый yield, последовательно |
| Память для больших данных | Хранит все значения | Хранит одно значение за раз |
yield приостанавливает, а не завершает
def three_things():
print("about to yield first")
yield "first"
print("about to yield second")
yield "second"
print("about to yield third")
yield "third"
print("generator exhausted")
for item in three_things():
print("got:", item)Вывод:
about to yield first
got: first
about to yield second
got: second
about to yield third
got: third
generator exhaustedОбратите внимание на операторы print между yield — обычный код выполняется между каждым приостановлением.
Генераторные функции против списков
Рассмотрим генерацию первых n квадратных чисел. Используя список:
def squares_list(n):
result = []
for i in range(1, n + 1):
result.append(i * i)
return result
print(squares_list(5)) # [1, 4, 9, 16, 25]Используя генератор:
def squares_gen(n):
for i in range(1, n + 1):
yield i * i
gen = squares_gen(5)
print(list(gen)) # [1, 4, 9, 16, 25]Оба производят одинаковые значения, но версия с генератором:
- Использует O(1) памяти независимо от
n(версия со списком использует O(n)) - Начинает производить значения немедленно, не ожидая построения всей коллекции
- Может представлять бесконечные последовательности (список не может)
Когда выбирать генератор
Используйте генератор, когда:
- Вам нужно пройти по значениям только один раз.
- Последовательность достаточно велика, чтобы хранить её всю в памяти было важно.
- Вы строите конвейер обработки данных (один генератор подаёт данные в другой).
- Последовательность потенциально бесконечна (например, чтение строк лога из живого файла).
Используйте список, когда:
- Вам нужен произвольный доступ по индексу.
- Вам нужно проходить по одной и той же последовательности несколько раз.
- Вам нужны
len(), срезы или сортировка на месте.
Генераторные выражения
Генераторное выражение относится к генераторам так же, как генератор списков относится к спискам. Синтаксис идентичен, за исключением того, что вместо квадратных скобок используются круглые:
# List comprehension — builds the full list immediately
squares_list = [x * x for x in range(1, 6)]
# Generator expression — lazy, produces one value at a time
squares_gen = (x * x for x in range(1, 6))
print(type(squares_list)) # <class 'list'>
print(type(squares_gen)) # <class 'generator'>
print(list(squares_gen)) # [1, 4, 9, 16, 25]Генераторные выражения наиболее полезны, когда их передают непосредственно в функцию, которая принимает итерируемый объект:
total = sum(x * x for x in range(1, 101)) # sum of squares 1..100
print(total) # 338350Дополнительные скобки не нужны, когда генераторное выражение является единственным аргументом вызова функции.
Фильтрация с помощью генераторных выражений
evens = (x for x in range(20) if x % 2 == 0)
print(list(evens)) # [0, 2, 4, 6, 8, 10, 12, 14, 16, 18]Бесконечные генераторы
Поскольку генератор производит значения лениво, он может представлять последовательность без конца. Классический пример — бесконечный счётчик:
def counter(start=0):
n = start
while True:
yield n
n += 1
gen = counter(10)
print(next(gen)) # 10
print(next(gen)) # 11
print(next(gen)) # 12Чтобы потребить только часть бесконечного генератора, используйте itertools.islice или выйдите из цикла:
import itertools
gen = counter(1)
first_five = list(itertools.islice(gen, 5))
print(first_five) # [1, 2, 3, 4, 5]Практический бесконечный генератор — последовательность Фибоначчи:
def fibonacci():
a, b = 0, 1
while True:
yield a
a, b = b, a + b
fib = fibonacci()
print([next(fib) for _ in range(10)])
# [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]yield from — делегирование подгенератору
yield from позволяет генератору делегировать работу другому итерируемому объекту, прозрачно передавая каждое значение:
def first_part():
yield 1
yield 2
def second_part():
yield 3
yield 4
def combined():
yield from first_part()
yield from second_part()
print(list(combined())) # [1, 2, 3, 4]yield from также работает с любым итерируемым объектом, не только с генераторами:
def flatten(nested):
for sublist in nested:
yield from sublist
data = [[1, 2], [3, 4], [5, 6]]
print(list(flatten(data))) # [1, 2, 3, 4, 5, 6]yield from чище, чем вложенный цикл for по подитерируемому объекту, и корректно перенаправляет вызовы send() и throw() делегированному генератору (важно для паттернов корутин).
Передача значений в генератор
Генераторы — двусторонние каналы. Метод .send(value) возобновляет генератор и передаёт значение обратно в виде результата выражения yield:
def accumulator():
total = 0
while True:
value = yield total # yield sends total out; receives value in
if value is None:
break
total += value
gen = accumulator()
next(gen) # prime the generator (advance to first yield)
print(gen.send(10)) # 10
print(gen.send(20)) # 30
print(gen.send(5)) # 35Правила для .send():
- Необходимо вызвать
next(gen)(илиgen.send(None)) один раз, чтобы продвинуть генератор до первогоyield, прежде чем можно будет отправить значение, отличное отNone. send(None)эквивалентенnext().- Отправленное значение становится результатом выражения
yieldв левой части.
Состояние генератора и исчерпание
Объект-генератор имеет жизненный цикл с четырьмя состояниями:
| Состояние | Описание |
|---|---|
| Создан | Генераторная функция вызвана, тело ещё не запущено |
| Выполняется | В данный момент выполняется (внутри вызова next() или send()) |
| Приостановлен | Остановлен на yield; возобновится при следующем next() |
| Закрыт | Тело завершено или вызван .close(); вызывает StopIteration |
После исчерпания повторная итерация генератора ничего не производит:
gen = (x for x in range(3))
print(list(gen)) # [0, 1, 2]
print(list(gen)) # [] — already exhaustedЕсли вам нужно пройти по выводу генератора более одного раза, либо сначала преобразуйте его в список, либо пересоздайте генератор.
return внутри генератора
Оператор return внутри генератора корректно завершает итерацию. Значение, переданное в return, становится атрибутом value исключения StopIteration (используется редко напрямую, но важно для делегирования с yield from):
def limited():
yield 1
yield 2
return "done" # StopIteration.value = "done"
gen = limited()
print(next(gen)) # 1
print(next(gen)) # 2
try:
next(gen)
except StopIteration as e:
print(e.value) # doneПрактические паттерны
Построчное чтение большого файла
def read_lines(filepath):
with open(filepath) as f:
for line in f:
yield line.rstrip("\n")
# Memory usage stays constant regardless of file size
for line in read_lines("/etc/hosts"):
if line.startswith("#"):
continue
print(line)Построение конвейера обработки данных
Генераторы естественным образом объединяются в конвейеры, где каждый этап преобразует поток:
def integers(n):
for i in range(1, n + 1):
yield i
def only_even(nums):
for n in nums:
if n % 2 == 0:
yield n
def squared(nums):
for n in nums:
yield n * n
# Compose: even squares from 1..20
pipeline = squared(only_even(integers(20)))
print(list(pipeline))
# [4, 16, 36, 64, 100, 144, 196, 256, 324, 400]Каждый этап ленивый — значения проходят через конвейер по одному, без построения промежуточных списков.
Разбивка итерируемого объекта на части
def chunks(iterable, size):
chunk = []
for item in iterable:
chunk.append(item)
if len(chunk) == size:
yield chunk
chunk = []
if chunk:
yield chunk
data = list(range(10))
for batch in chunks(data, 3):
print(batch)
# [0, 1, 2]
# [3, 4, 5]
# [6, 7, 8]
# [9]Генераторы против итераторов и генераторов списков
| Возможность | Класс-итератор | Генераторная функция | Генераторное выражение |
|---|---|---|---|
| Синтаксис | Класс с __iter__/__next__ | def + yield | (expr for x in ...) |
| Многословность | Высокая | Низкая | Очень низкая |
| Управление состоянием | Ручное | Автоматическое | Автоматическое |
| Многострочная логика | Да | Да | Нет (одно выражение) |
| Бесконечные последовательности | Да | Да | Да |
| Читаемость для сложной логики | Да | Да | Нет |
Для чего-либо сложнее простого преобразования или фильтрации генераторная функция читается лучше, чем генераторное выражение. Для сложной итерации с состоянием генераторная функция почти всегда предпочтительнее написания полного класса-итератора — см. Итераторы Python для подхода на основе классов.
Генераторные выражения хорошо сочетаются с генераторами списков и генераторами словарей/множеств. Декораторы также могут оборачивать генераторные функции, добавляя кэширование или трассировку.