W3docs

Генераторы 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

returnyield
Тип функцииОбычнаяГенераторная
Выполнение после вызоваВыполняется до концаПриостанавливается на 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():

  1. Необходимо вызвать next(gen) (или gen.send(None)) один раз, чтобы продвинуть генератор до первого yield, прежде чем можно будет отправить значение, отличное от None.
  2. send(None) эквивалентен next().
  3. Отправленное значение становится результатом выражения 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 для подхода на основе классов.

Генераторные выражения хорошо сочетаются с генераторами списков и генераторами словарей/множеств. Декораторы также могут оборачивать генераторные функции, добавляя кэширование или трассировку.

Практика

Практика
Which of the following statements about Python generators are correct?
Which of the following statements about Python generators are correct?
Was this page helpful?