Генераторы и ключевое слово yield
В прошлом уроке мы писали итераторы через классы — с методами __iter__ и __next__, двумя классами, кучей кода. Теперь вы узнаете о генераторах — способе написать то же самое в разы короче, изящнее и мощнее. Всё благодаря одному волшебному слову: yield.
⚡ Что такое функция-генератор?
Посмотрите на обычную функцию и функцию-генератор — отличие всего одно:
🔧
Обычная функция
Содержит 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— делегирует поток значений из любого итерируемого объекта или другого генератора.- Генераторы идеально подходят для: бесконечных последовательностей, потоковой обработки, накопления состояния (Фибоначчи, суммы, скользящие средние).
👉 В следующем уроке мы познакомимся с генераторными выражениями — ещё более компактным способом создавать генераторы в одну строку, по аналогии со списковыми включениями.