Справочник по Python · модуль 7 · итераторы и генераторы
Глава 7.3
Передача значений
в генератор: send()
До сих пор генератор только отдавал значения. Метод send() открывает движение в обратную сторону: он передаёт значение внутрь приостановленного генератора, и оно становится результатом выражения yield.
- Категория
- Генераторы
- Уровень
- Продвинутый
- Связанные темы
- yield, next(), объект None
- Зачем это знать
- Строить генераторы, которые накапливают и принимают данные
§1Генератор как собеседник
В главе 7.2 генератор работал как источник: next() запрашивал очередное значение, генератор его выдавал и засыпал до следующего запроса. Связь была односторонней — от генератора наружу.
Но приостановленная функция сохраняет всё своё состояние, а значит, ей можно и передать что-то в момент пробуждения. Тогда генератор превращается из источника в сопрограмму — функцию, которая ведёт с вызывающим кодом диалог: получает значение, обрабатывает, отдаёт ответ и снова засыпает.
§2yield — выражение, а не команда
Ключ ко всему — то, что yield в Python является выражением и имеет результат. Его можно записать справа от знака присваивания.
yield total отдать значение наружу
value = yield total отдать значение наружу и,
проснувшись, получить обратно
Читать вторую строку следует так: выполнение останавливается на yield, значение total уходит наружу; когда генератор разбудят, присланное значение станет результатом выражения и запишется в value.
§3Что приходит при обычном next()
Если генератор разбудили обычным next(), ничего внутрь не передавалось. В этом случае результатом выражения yield становится None — объект из главы 6.13, обозначающий отсутствие значения.
Поэтому в генераторах-сопрограммах постоянно встречается проверка if value is None: она отличает «просто попросили следующее значение» от «прислали данные».
§4Метод send()
Метод send(value) делает два дела сразу: передаёт значение внутрь и возобновляет работу генератора до следующего yield.
def echo(): while True: received = yield print("получено:", received) gen = echo() next(gen) # доводим до первого yield gen.send("привет") gen.send(42)
получено: привет получено: 42
§5Накопитель: полный пример
Теперь генератор, который и принимает значения, и отдаёт результат: он копит сумму присланных чисел.
def accumulator(): total = 0 while True: value = yield total if value is None: break total += value gen = accumulator() print(next(gen)) print(gen.send(10)) print(gen.send(20))
0 10 30
Обратите внимание: переменная total не хранится ни в каком объекте снаружи. Она живёт внутри приостановленной функции — генератор помнит своё состояние сам.
§6Разбор по шагам
accumulator()создаёт генератор; тело функции ещё не выполнялось.next(gen)запускает тело:total = 0, вход в цикл, остановка наyield total. Наружу уходит0.gen.send(10)будит генератор,valueполучает10,totalстановится10, новый виток цикла, остановка наyield total. Наружу уходит10.gen.send(20)— то же самое:totalстановится30, наружу уходит30.
Каждый вызов send() — один цикл обмена: значение внутрь, значение наружу.
§7Обязательный первый next()
Только что созданный генератор ещё не дошёл до первого yield, а значит, передавать в него нечего: внутри нет выражения, которое приняло бы значение. Попытка сразу вызвать send() с данными — ошибка.
gen = accumulator() gen.send(5)
TypeError: can't send non-None value to a just-started generator
Поэтому первый шаг любой сопрограммы — next(gen): он доводит выполнение до первого yield и ставит генератор в позицию, из которой тот способен что-то принять. Этот приём называют запуском (priming) генератора.
§8next(gen) — это send(None)
Два способа возобновить генератор — на самом деле один.
next(gen) == gen.send(None)
Отсюда и правило §3: при обычном переборе внутрь приходит None. И отсюда же — почему gen.send(None) допустим для только что созданного генератора, а gen.send(5) нет: None означает «ничего не передаю».
§9Что возвращает send()
Возвращаемое значение send() — то, что генератор отдал на следующем yield. Не эхо присланного, а ответ генератора.
| Действие | Что идёт внутрь | Что возвращается |
|---|---|---|
next(gen) | None | значение следующего yield |
gen.send(x) | x | значение следующего yield |
gen.close() | исключение GeneratorExit | ничего |
§10yield без значения
Если генератору нечего отдавать, а нужно только принимать, yield пишут без выражения — как в примере echo из §4. Наружу тогда уходит None, и вызывающий код обычно этот результат игнорирует.
Такие генераторы называют приёмниками: они собирают присылаемые данные и что-то с ними делают — записывают в файл, накапливают статистику, передают дальше.
def collector(target): while True: item = yield target.append(item) log = [] sink = collector(log) next(sink) sink.send("вход") sink.send("выход") print(log)
['вход', 'выход']
§11Завершение и StopIteration
В примере из §5 break срабатывает, когда внутрь приходит None. Тело функции доходит до конца, генератор завершается — и вызывающая сторона узнаёт об этом привычным способом.
gen = accumulator() next(gen) gen.send(3) try: gen.send(None) except StopIteration: print("генератор завершился")
генератор завершился
То есть None в этом генераторе играет роль условного знака «работа окончена». Есть и прямой способ остановить генератор снаружи — метод close(); о нём и о гарантированной уборке речь пойдёт в следующей главе.
§12Когда send() уместен
send() — инструмент узкого применения, и это нормально. Он нужен там, где генератор должен помнить состояние между порциями данных, а данные приходят не сразу.
def moving_average(): total = 0 count = 0 average = None while True: value = yield average total += value count += 1 average = total / count avg = moving_average() next(avg) print(avg.send(10)) print(avg.send(20)) print(avg.send(30))
10.0 15.0 20.0
Ни total, ни count не хранятся снаружи и не передаются в вызовах — вся «память» задачи находится внутри приостановленной функции. В этом и состоит выигрыш: вызывающему коду достаточно присылать очередное измерение.
| Задача | Подходит ли send() |
|---|---|
| перебрать большой файл построчно | нет, достаточно обычного yield |
| копить скользящее среднее по потоку измерений | да, состояние живёт внутри генератора |
| разобрать поток данных, приходящий кусками | да, генератор помнит, где остановился разбор |
| просто сложить числа списка | нет, это работа для sum() |
Заметьте: генератор, в который что-то присылают, нельзя пройти обычным циклом for — цикл умеет только вызывать next(). Такой сопрограммой всегда управляют вручную.
§13Частые ошибки
| Что написано | Что произойдёт | Как правильно |
|---|---|---|
gen.send(5) сразу после создания | TypeError: генератор не запущен | сначала next(gen) |
yield total вместо value = yield total | присланное значение будет отброшено | записать результат yield в переменную |
ждать от send() присланного значения | вернётся то, что генератор отдал на следующем yield | читать возврат как ответ генератора |
перебирать сопрограмму циклом for | внутрь всегда будет приходить None | управлять вручную через send() |
забыть проверку if value is None | обычный next() испортит накопленное состояние | отличать None от присланных данных |
вызвать send() после завершения генератора | StopIteration | перехватить исключение или создать генератор заново |
§14Проверьте себя
Что делает send()?
Передаёт значение внутрь приостановленного генератора и возобновляет его работу до следующего yield.
Куда попадает присланное значение?
Оно становится результатом выражения yield, на котором генератор был приостановлен.
Почему перед send() нужен next()?
Новый генератор ещё не дошёл до первого yield, поэтому принять значение некому.
Что приходит внутрь при обычном next()?
None: next(gen) равносилен gen.send(None).
Что возвращает сам вызов send()?
Значение, которое генератор отдал на следующем yield, — его ответ, а не присланное значение.
§15Шпаргалка
value = yield resultnext(gen)answer = gen.send(x)next(gen) == gen.send(None)received = yieldИтог главы
yield — выражение, и его результатом становится то, что прислали генератору методом send(). Это превращает генератор в сопрограмму: он хранит состояние между вызовами, принимает данные и отвечает очередным значением. Перед первым send() генератор обязательно запускают вызовом next(), который равносилен send(None). Инструмент узкий, но незаменимый там, где данные приходят порциями, а промежуточное состояние удобнее держать внутри самой функции.