Антон Ларичев
Продвинутые генераторы в Python — send, throw, close и корутины
Введение
Генераторы в Python — это не только ленивые итераторы. Благодаря методам send(), throw() и close() они становятся полноценными корутинами, способными принимать данные извне, обрабатывать исключения и управлять своим жизненным циклом. Эти возможности лежат в основе асинхронного программирования и многих продвинутых паттернов.
В этой статье мы разберём методы управления генераторами, создание корутин и практические паттерны, которые выходят за рамки простого yield.
Метод send() — отправка данных в генератор
Метод send() позволяет не только получать значения из генератора, но и передавать данные обратно. Значение, переданное через send(), становится результатом выражения yield внутри генератора:
def accumulator():
"""Генератор-аккумулятор: принимает числа и возвращает текущую сумму"""
total = 0
while True:
# yield возвращает total и принимает новое значение в value
value = yield total
if value is not None:
total += value
acc = accumulator()
# Первый вызов — инициализация генератора (send(None) или next())
next(acc) # 0 — начальное значение total
print(acc.send(10)) # 10
print(acc.send(20)) # 30
print(acc.send(5)) # 35
Важно: первый вызов генератора должен быть next() или send(None), чтобы выполнение дошло до первого yield. Попытка отправить значение в «неинициализированный» генератор вызовет TypeError.
Паттерн корутины
Корутина — это генератор, который в основном принимает данные через send(), а не генерирует последовательность. Типичная корутина использует бесконечный цикл с yield:
def running_average():
"""Корутина для вычисления скользящего среднего"""
total = 0.0
count = 0
average = None
while True:
value = yield average
total += value
count += 1
average = total / count
# Используем корутину
avg = running_average()
next(avg) # Инициализация
print(avg.send(10)) # 10.0
print(avg.send(20)) # 15.0
print(avg.send(30)) # 20.0
print(avg.send(40)) # 25.0
Декоратор для автоматической инициализации
Чтобы не вызывать next() вручную, можно создать декоратор:
from functools import wraps
def coroutine(func):
"""Декоратор для автоматической инициализации корутины"""
@wraps(func)
def wrapper(*args, **kwargs):
gen = func(*args, **kwargs)
next(gen) # Инициализируем генератор
return gen
return wrapper
@coroutine
def filter_values(predicate):
"""Корутина-фильтр: пропускает только подходящие значения"""
results = []
while True:
value = yield results
if predicate(value):
results.append(value)
# Фильтруем только положительные числа
f = filter_values(lambda x: x > 0)
# Не нужен вызов next() — декоратор сделал это за нас
print(f.send(5)) # [5]
print(f.send(-3)) # [5]
print(f.send(10)) # [5, 10]
Если вы хотите детальнее изучить Python и разобраться с продвинутыми концепциями — приходите на наш большой курс Python-разработчик с нуля. На курсе 180 уроков и 80 упражнений, AI-тренажёры для практики 24/7, решение задач с живым ревью наставника, еженедельные встречи с менторами.
Метод throw() — отправка исключений в генератор
Метод throw() позволяет выбросить исключение внутри генератора в точке yield. Генератор может обработать это исключение или пропустить его дальше:
def resilient_processor():
"""Генератор с обработкой ошибок"""
while True:
try:
value = yield
print(f"Обработано: {value}")
except ValueError as e:
print(f"Ошибка значения: {e}")
except GeneratorExit:
print("Генератор завершён")
return
proc = resilient_processor()
next(proc)
proc.send("данные") # Обработано: данные
proc.throw(ValueError, "неверный формат") # Ошибка значения: неверный формат
proc.send("ещё данные") # Обработано: ещё данные
Метод close() — завершение генератора
Метод close() выбрасывает исключение GeneratorExit внутри генератора, что приводит к его завершению:
def resource_manager():
"""Генератор, управляющий ресурсом"""
print("Ресурс открыт")
try:
while True:
data = yield
print(f"Записано: {data}")
except GeneratorExit:
print("Ресурс закрыт — очистка завершена")
rm = resource_manager()
next(rm) # Ресурс открыт
rm.send("строка 1") # Записано: строка 1
rm.send("строка 2") # Записано: строка 2
rm.close() # Ресурс закрыт — очистка завершена
Метод close() полезен для корректного освобождения ресурсов: закрытия файлов, сетевых соединений и других объектов.
Генератор как конечный автомат
Генераторы с send() отлично подходят для реализации конечных автоматов:
def traffic_light():
"""Генератор-светофор: переключает состояния по сигналу"""
state = "красный"
transitions = {
"красный": "зелёный",
"зелёный": "жёлтый",
"жёлтый": "красный"
}
while True:
command = yield state
if command == "next":
state = transitions[state]
elif command == "reset":
state = "красный"
light = traffic_light()
print(next(light)) # красный
print(light.send("next")) # зелёный
print(light.send("next")) # жёлтый
print(light.send("next")) # красный
print(light.send("next")) # зелёный
print(light.send("reset")) # красный
yield from и двусторонний обмен
Конструкция yield from не просто делегирует итерацию — она пробрасывает send() и throw() к вложенному генератору:
def inner():
"""Вложенный генератор, принимающий данные"""
result = []
while True:
value = yield
if value is None:
return result # Возвращает результат через StopIteration
result.append(value * 2)
def outer():
"""Внешний генератор, делегирующий работу"""
# yield from пробрасывает send() и собирает return
result = yield from inner()
yield result
gen = outer()
next(gen)
gen.send(1)
gen.send(2)
gen.send(3)
print(gen.send(None)) # [2, 4, 6]
Генераторы и itertools
Модуль itertools предоставляет инструменты, которые хорошо сочетаются с генераторами:
import itertools
def sensor_readings():
"""Имитация бесконечного потока показаний датчика"""
import random
while True:
yield random.uniform(20.0, 30.0)
# Берём первые 5 показаний
readings = itertools.islice(sensor_readings(), 5)
for r in readings:
print(f"{r:.1f}°C")
# Группировка по условию
data = [1, 1, 2, 3, 3, 3, 4, 5, 5]
for key, group in itertools.groupby(data):
print(f"{key}: {list(group)}")
Частые ошибки
- Вызов
send()без инициализации. Перед первымsend()с ненулевым аргументом нужно вызватьnext()илиsend(None). - Игнорирование
GeneratorExitвtry/finally. Если генератор содержитtry/finally, блокfinallyвыполнится приclose(). Убедитесь, что он не выбрасывает исключения. - Бесконечный генератор без
close(). Если генератор управляет ресурсами, всегда завершайте его черезclose()или используйте контекстный менеджер.
Частозадаваемые вопросы
Чем корутина на генераторах отличается от async/await?
Корутины на генераторах — предшественник async/await. Они работают через yield и send(), а async/await — через специальный синтаксис, введённый в Python 3.5. Современный асинхронный код лучше писать через async/await, но понимание генераторных корутин помогает разобраться в механизмах Python.
Когда использовать send() вместо аргумента функции?
Используйте send(), когда генератор должен сохранять состояние между вызовами и принимать данные порционно. Это полезно для потоковой обработки, конечных автоматов и конвейеров данных.
Безопасно ли вызывать close() на уже завершённом генераторе?
Да, повторный вызов close() на завершённом генераторе не вызывает ошибок.
Заключение
Продвинутые возможности генераторов — send(), throw(), close() и yield from — превращают их из простых итераторов в мощный инструмент для управления потоком выполнения. Корутины на генераторах, конечные автоматы и двусторонний обмен данными — всё это доступно благодаря протоколу генераторов Python.
Для закрепления навыков работы с генераторами и освоения асинхронного программирования рекомендуем курс Python-разработчик с нуля. В первых 3 модулях курса доступно бесплатное содержание, что позволяет разобраться с основами языка и понять структуру курса до покупки полного доступа.
Постройте личный план изучения Python до уровня Middle — бесплатно!
Python — часть карты развития Backend
100+ шагов развития
30 бесплатных лекций
300 бонусных рублей на счет
Все гайды по Python
Лучшие курсы по теме

Основы Python
Антон Ларичев
Nest.js с нуля
Антон Ларичев