$ sudo teach IT
МОДУЛЬ 8 · УРОК 3

Генераторы и ключевое слово yield

В прошлом уроке мы писали итераторы через классы — с методами __iter__ и __next__, двумя классами, кучей кода. Теперь вы узнаете о генераторах — способе написать то же самое в разы короче, изящнее и мощнее. Всё благодаря одному волшебному слову: yield.

⏱ ~50 минут 🎓 Для новичков 🐍 Python 3

⚡ Что такое функция-генератор?

Посмотрите на обычную функцию и функцию-генератор — отличие всего одно:

🔧

Обычная функция

Содержит return. Вызов сразу выполняет весь код и возвращает одно значение. После return функция завершается — навсегда.

✨

Функция-генератор

Содержит yield. Вызов не выполняет код вообще — только создаёт генераторный объект. Код запускается лишь тогда, когда вы запрашиваете следующее значение.

Правило простое: если в теле функции есть хотя бы одно слово yield — Python автоматически превращает её в функцию-генератор. Не важно, где именно стоит yield — в начале, середине или конце.

💡 Вызов функции-генератора возвращает генераторный объект — не значение, не список, а специальный объект. Он реализует протокол итератора: у него есть __iter__ и __next__. Дальше с ним работают через next() или цикл for.

def simple_generator():
    yield 10
    yield 20
    yield 30

# Вызываем функцию — получаем объект, а не значение!
gen = simple_generator()
print(type(gen))    # <class 'generator'>
print(gen)          # <generator object simple_generator at 0x...>

# Запрашиваем значения через next():
print(next(gen))    # 10
print(next(gen))    # 20
print(next(gen))    # 30
# print(next(gen))  # StopIteration — значения кончились

# Или просто в цикле for:
for value in simple_generator():
    print(value)
# Вывод: 10, 20, 30

🔍 yield vs return: как работает «пауза»

Главное, что нужно понять: yield — это не return. return завершает функцию навсегда. yield приостанавливает её — сохраняет всё состояние (все переменные, текущую строку кода) и ждёт следующего запроса.

🎬 Аналогия: видео на паузе

return — это как выключить видео. Фильм завершён. Чтобы посмотреть снова — нужно запустить заново с начала.
yield — это как нажать «Пауза». Фильм остановлен на конкретном кадре. Нажмёте «Play» — продолжится ровно с того места, где остановился. Все «актёры» на экране остались на своих позициях.

Посмотрим, как выполняется код шаг за шагом:

def countdown(n):
    print(f"Начинаем обратный отсчёт от {n}")
    while n > 0:
        print(f"  → сейчас перед yield, n = {n}")
        yield n                              # ПАУЗА: возвращаем n, ждём next()
        print(f"  → продолжаем после yield, n всё ещё {n}")
        n -= 1
    print("Отсчёт завершён!")

gen = countdown(3)

print("=== Вызов next() #1 ===")
val = next(gen)
print(f"Получили: {val}")

print("\n=== Вызов next() #2 ===")
val = next(gen)
print(f"Получили: {val}")

print("\n=== Вызов next() #3 ===")
val = next(gen)
print(f"Получили: {val}")

print("\n=== Вызов next() #4 ===")
try:
    val = next(gen)
except StopIteration:
    print("StopIteration — генератор исчерпан")

# Вывод:
# === Вызов next() #1 ===
#   → сейчас перед yield, n = 3
# Получили: 3
#
# === Вызов next() #2 ===
#   → продолжаем после yield, n всё ещё 3
#   → сейчас перед yield, n = 2
# Получили: 2
#
# === Вызов next() #3 ===
#   → продолжаем после yield, n всё ещё 2
#   → сейчас перед yield, n = 1
# Получили: 1
#
# === Вызов next() #4 ===
#   → продолжаем после yield, n всё ещё 1
# Отсчёт завершён!
# StopIteration — генератор исчерпан

🎯 Ключевое наблюдение: после первого next() генератор остановился прямо на строке yield n. Переменная n сохранила значение 3. Второй next() продолжил выполнение со следующей строки — print(f"→ продолжаем...") — и только потом дошёл до нового yield.

Когда функция доходит до конца тела (или встречает return без значения) — Python автоматически поднимает StopIteration. Именно поэтому цикл for останавливается сам.

⚖️ Сравнение: класс-итератор vs функция-генератор

Напишем одну и ту же задачу двумя способами: генератор чётных чисел до N. Сравните объём кода:

# ===== СПОСОБ 1: класс-итератор (старый подход) =====

class EvenNumbersIterator:
    """Итератор чётных чисел от 0 до limit (не включая)."""

    def __init__(self, limit):
        self.limit = limit
        self.current = 0

    def __iter__(self):
        return self

    def __next__(self):
        while self.current < self.limit:
            value = self.current
            self.current += 2
            return value
        raise StopIteration


class EvenNumbers:
    """Iterable-обёртка для EvenNumbersIterator."""

    def __init__(self, limit):
        self.limit = limit

    def __iter__(self):
        return EvenNumbersIterator(self.limit)


# Использование
for n in EvenNumbers(10):
    print(n)
# Вывод: 0, 2, 4, 6, 8


# ===== СПОСОБ 2: функция-генератор (новый подход) =====

def even_numbers(limit):
    """Генератор чётных чисел от 0 до limit (не включая)."""
    current = 0
    while current < limit:
        yield current
        current += 2


# Использование — точно такое же!
for n in even_numbers(10):
    print(n)
# Вывод: 0, 2, 4, 6, 8

😓

Класс-итератор

  • 2 класса, ~25 строк
  • Нужно помнить про __iter__, __next__
  • Нужно вручную хранить состояние (self.current)
  • Нужно поднимать StopIteration

😊

Функция-генератор

  • 1 функция, ~5 строк
  • Только yield — всё остальное автоматически
  • Состояние сохраняется само (локальные переменные)
  • StopIteration поднимается автоматически

🌟 Суть генераторов: Python берёт на себя всю «скучную» часть — создание объекта, управление состоянием, отправку StopIteration. Вы пишете только логику: что генерировать и когда останавливаться.

🧠 Состояние сохраняется между вызовами — это суть

Самое важное свойство генераторов — локальные переменные живут между вызовами next(). Это не обычная функция, где каждый вызов создаёт новый стек. Генератор — это «замороженная» функция.

Что именно сохраняется между вызовами:

📌

Текущая позиция

На какой строке остановился код

📦

Все локальные переменные

Числа, строки, списки — всё что объявлено внутри

🔄

Состояние циклов

Счётчики, индексы, флаги внутри while/for

def accumulator():
    """Генератор, который накапливает сумму — состояние живёт!"""
    total = 0
    values = [1, 5, 3, 8, 2]

    for v in values:
        total += v             # total накапливается между вызовами!
        print(f"  добавили {v}, total = {total}")
        yield total            # возвращаем текущую накопленную сумму

gen = accumulator()

print(next(gen))   # → добавили 1, total = 1  → получили 1
print(next(gen))   # → добавили 5, total = 6  → получили 6
print(next(gen))   # → добавили 3, total = 9  → получили 9
# Переменная total продолжает накапливаться — она не сбросилась!

💡 Именно поэтому генераторы идеальны для задач, где нужно помнить историю: накопление, скользящие средние, последовательности (Фибоначчи, простые числа), потоковая обработка данных.

🌀 Классический пример: генератор Фибоначчи

Последовательность Фибоначчи — идеальная задача для генератора. Каждое следующее число зависит от двух предыдущих. Нам нужно помнить их между вызовами — генератор делает это естественно.

Напомним последовательность: 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, … Каждый следующий элемент = сумма двух предыдущих.

def fibonacci():
    """
    Бесконечный генератор чисел Фибоначчи.
    Работает вечно — останавливать нужно вам самим.
    """
    a, b = 0, 1
    while True:            # бесконечный цикл — генератор управляет остановкой
        yield a            # отдаём текущее число
        a, b = b, a + b    # сдвигаемся вперёд: новый a = старый b, новый b = сумма

# Берём первые 10 чисел
gen = fibonacci()
for _ in range(10):
    print(next(gen), end=" ")
# Вывод: 0 1 1 2 3 5 8 13 21 34


# Или с ограничением через функцию-обёртку:
def fibonacci_up_to(limit):
    """Генератор Фибоначчи до числа limit (не включая)."""
    a, b = 0, 1
    while a < limit:
        yield a
        a, b = b, a + b

print(list(fibonacci_up_to(100)))
# Вывод: [0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89]


# Поиск: первое число Фибоначчи больше 1000:
gen = fibonacci()
fib = next(gen)
while fib <= 1000:
    fib = next(gen)
print(f"Первое Фибоначчи > 1000: {fib}")   # 1597

🌟 Обратите внимание: первая версия — бесконечный генератор. Он никогда не поднимает StopIteration сам. Это нормально! Цикл while True с yield внутри работает как бесконечный источник значений. Вы управляете остановкой снаружи — через range(), условие или break.

🔗 yield from — делегирование другому генератору

Иногда нужно, чтобы один генератор передал управление другому генератору или итерируемому объекту. Для этого есть специальный синтаксис: yield from.

📌 yield from iterable — это короткая запись для: «возьми каждый элемент из iterable и передай его наружу, один за одним». Всё что for x in iterable: yield x — но в одну строку.

# ===== Пример 1: yield from заменяет внутренний цикл =====

def flatten_manual(nested):
    """Разворачиваем вложенный список — вручную."""
    for sublist in nested:
        for item in sublist:
            yield item

def flatten_yf(nested):
    """Разворачиваем вложенный список — через yield from."""
    for sublist in nested:
        yield from sublist    # передаём каждый элемент sublist наружу

data = [[1, 2, 3], [4, 5], [6, 7, 8, 9]]

print(list(flatten_manual(data)))  # [1, 2, 3, 4, 5, 6, 7, 8, 9]
print(list(flatten_yf(data)))      # [1, 2, 3, 4, 5, 6, 7, 8, 9]


# ===== Пример 2: делегирование другому генератору =====

def first_part():
    yield "A"
    yield "B"
    yield "C"

def second_part():
    yield "X"
    yield "Y"

def combined():
    """Объединяем два генератора в один поток."""
    yield from first_part()    # сначала все значения из first_part
    yield from second_part()   # потом все из second_part
    yield "Z"                  # и ещё одно своё значение

print(list(combined()))
# Вывод: ['A', 'B', 'C', 'X', 'Y', 'Z']


# ===== Пример 3: yield from с любым итерируемым =====

def chain_all(*iterables):
    """Объединяет любое количество итерируемых объектов."""
    for it in iterables:
        yield from it    # работает с list, str, range, генератором — с чем угодно

result = list(chain_all([1, 2, 3], "abc", range(4, 7)))
print(result)
# Вывод: [1, 2, 3, 'a', 'b', 'c', 4, 5, 6]

💡 Когда использовать yield from: всегда, когда нужно «прозрачно» передать поток значений из вложенного итерируемого. Это делает код чище и позволяет строить генераторные конвейеры — цепочки обработки данных.

🔎 Что именно возвращает вызов функции-генератора?

Новички часто путаются: «Я вызвал генератор — почему ничего не напечаталось?». Разберём это один раз и навсегда.

def my_gen():
    print("Старт генератора")
    yield 1
    print("После первого yield")
    yield 2
    print("Конец генератора")

# Вызываем функцию — код ВНУТРИ не выполняется вообще!
g = my_gen()
print("Создали объект:", type(g))
# Вывод: Создали объект: <class 'generator'>
# Строка "Старт генератора" ещё не напечатана!

# Только first next() — запускает код до первого yield
print("--- Первый next() ---")
val = next(g)
print("Вернул:", val)
# Вывод:
# --- Первый next() ---
# Старт генератора
# Вернул: 1

# Второй next() — продолжает с того места
print("--- Второй next() ---")
val = next(g)
print("Вернул:", val)
# Вывод:
# --- Второй next() ---
# После первого yield
# Вернул: 2

# Третий next() — доходит до конца, поднимает StopIteration
print("--- Третий next() ---")
try:
    next(g)
except StopIteration:
    print("StopIteration: генератор исчерпан")
# Вывод:
# --- Третий next() ---
# Конец генератора
# StopIteration: генератор исчерпан

❌ Частая ошибка

Думать, что result = my_gen() запустит код функции и result будет содержать значение. Нет — result это объект-генератор. Код функции ещё не выполнен ни строчки.

✅ Правильное понимание

my_gen() создаёт «замороженную» функцию, готовую к запуску. Запуск происходит лишь при первом next() — и только до ближайшего yield.

Если вы хотите получить все значения сразу в список — используйте list():

def squares(n):
    for i in range(n):
        yield i * i

# Способ 1: список сразу
result = list(squares(5))
print(result)    # [0, 1, 4, 9, 16]

# Способ 2: в цикле for (ленивое потребление — по одному)
for sq in squares(5):
    print(sq)    # 0, 1, 4, 9, 16

✅ Итоги урока

  • Функция-генератор — это функция с хотя бы одним yield. Вызов возвращает генераторный объект, а не значение. Код функции не выполняется до первого next().
  • yield приостанавливает функцию и возвращает значение. Все локальные переменные и текущая позиция в коде сохраняются до следующего вызова next().
  • Когда функция доходит до конца (или до return) — Python автоматически поднимает StopIteration.
  • Генератор vs класс-итератор: тот же результат, но в разы меньше кода — не нужно писать __iter__, __next__, хранить состояние вручную.
  • yield from iterable — делегирует поток значений из любого итерируемого объекта или другого генератора.
  • Генераторы идеально подходят для: бесконечных последовательностей, потоковой обработки, накопления состояния (Фибоначчи, суммы, скользящие средние).

👉 В следующем уроке мы познакомимся с генераторными выражениями — ещё более компактным способом создавать генераторы в одну строку, по аналогии со списковыми включениями.