Объясните механизм: куда попадает значение, переданное генератору методом send?

Объясните механизм: куда попадает значение, переданное генератору методом send?

Проходите собеседования с ИИ помощником Hintsage

Краткий ответ

Значение, переданное через send, становится результатом выражения yield, на котором генератор был приостановлен. Метод возобновляет выполнение генератора с этого места, поэтому присваивание после yield получает переданное значение.

В только что созданный генератор нельзя передать ненулевое значение: сначала его нужно запустить через next() или send(None).

Исторический контекст

Изначально генераторы в Python решали задачу ленивого получения последовательностей: они позволяли выдавать элементы по одному и не хранить весь результат в памяти. Такой подход уменьшает потребление памяти и позволяет обрабатывать потенциально большие или бесконечные потоки.

Позднее протокол генераторов расширили двусторонним обменом. Методы send, throw и close позволили не только получать значения от генератора, но и передавать ему данные или управлять его завершением.

Постановка проблемы

Обычный вызов next() только возобновляет генератор и получает следующее значение. Для сценариев, где генератор должен реагировать на внешние данные, этого недостаточно: требуется передать информацию внутрь уже выполнявшегося вычисления.

Неверное понимание механизма приводит к ошибкам в инициализации. Ненулевое значение нельзя отправить в генератор до его первой остановки на yield, потому что ещё нет выражения yield, результатом которого оно могло бы стать.

Подробное решение

При выполнении генератора выражение yield одновременно выдаёт значение наружу и приостанавливает выполнение. Когда вызывается send(value), генератор продолжает работу, а выражение yield внутри него вычисляется как value.

def accumulator(): total = 0 while True: delta = yield total if delta is None: return total total += delta generator = accumulator() print(next(generator)) # 0 print(generator.send(3)) # 3 print(generator.send(2)) # 5

После первого next() генератор остановлен на yield total. Вызов send(3) подставляет число 3 вместо всего выражения yield total, поэтому переменная delta получает значение 3. Затем генератор продолжает работу до следующего yield.

send(None) эквивалентен обычному next() и используется для запуска или обычного возобновления генератора. Вызов send с ненулевым значением до первого yield приводит к TypeError.

Если переданное значение не соответствует ожидаемому формату, ошибка возникает уже внутри тела генератора. После завершения генератора последующие попытки возобновить его приводят к StopIteration.

Такой механизм полезен для специализированных генераторов, имитирующих сопрограммы, но он не заменяет современный asyncio: асинхронные функции и задачи предоставляют более явную модель планирования и ожидания операций ввода-вывода.

Ситуация из практики

Сервис разбирает поток сообщений и должен принимать от внешнего кода команды изменения режима обработки. Генератор может выдавать очередной результат, приостанавливаться на yield и получать новую настройку через send без передачи состояния через глобальные переменные.

Вариант с глобальным состоянием проще написать, но он ухудшает тестируемость и может привести к конфликтам при повторном использовании обработчика. Передача состояния через параметры обычной функции требует вручную хранить промежуточное состояние между вызовами.

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

Для небольшого последовательного конвейера выбран генератор с send, потому что состояние принадлежит одному экземпляру обработчика и не требует внешнего хранилища. Для конкурентной обработки и сетевого ввода-вывода предпочтительнее использовать asyncio, поскольку там управление задачами и ожиданием выражено средствами асинхронного протокола.

Что кандидаты часто упускают

  1. Чем отличаются send(None) и next()?

Функционально при возобновлении генератора они эквивалентны: оба продолжают выполнение до следующего yield и получают выданное значение. Разница проявляется в намерении: next() запрашивает следующий результат, а send(None) явно использует двусторонний интерфейс, не передавая содержательного значения.

  1. Что произойдёт, если вызвать send после завершения генератора?

Генератор не запустится заново. Вызов приведёт к StopIteration, как и повторный вызов next() после исчерпания генератора. Значение, возвращённое через return, доступно в исключении, возникшем при завершившем генератор вызове; последующие вызовы уже не повторяют это возвращаемое значение как новый результат.

  1. Можно ли передать генератору исключение вместо обычного значения?

Да, для этого предназначен метод throw. Он возобновляет генератор так, будто исключение возникло в точке приостановленного yield. Генератор может перехватить его через try/except, продолжить работу или завершиться; если исключение не обработано, оно выйдет наружу вызывающему коду. Это отличается от send, который передаёт обычное значение как результат выражения yield.