Модуль 7 · Итераторы и генераторы

Справочник по Python · модуль 7 · итераторы и генераторы

Глава 7.3

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.

main.pyгенератор печатает всё, что ему присылают
def echo():
    while True:
        received = yield
        print("получено:", received)

gen = echo()
next(gen)                # доводим до первого yield
gen.send("привет")
gen.send(42)
Выводзначения дошли внутрь
получено: привет
получено: 42

§5Накопитель: полный пример

Теперь генератор, который и принимает значения, и отдаёт результат: он копит сумму присланных чисел.

main.pyдвусторонний обмен
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Разбор по шагам

  1. accumulator() создаёт генератор; тело функции ещё не выполнялось.
  2. next(gen) запускает тело: total = 0, вход в цикл, остановка на yield total. Наружу уходит 0.
  3. gen.send(10) будит генератор, value получает 10, total становится 10, новый виток цикла, остановка на yield total. Наружу уходит 10.
  4. gen.send(20) — то же самое: total становится 30, наружу уходит 30.

Каждый вызов send() — один цикл обмена: значение внутрь, значение наружу.

§7Обязательный первый next()

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

main.pysend() до первого next()
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 выражается через send
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, и вызывающий код обычно этот результат игнорирует.

Такие генераторы называют приёмниками: они собирают присылаемые данные и что-то с ними делают — записывают в файл, накапливают статистику, передают дальше.

main.pyприёмник складывает всё в список
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. Тело функции доходит до конца, генератор завершается — и вызывающая сторона узнаёт об этом привычным способом.

main.pyсигнал к остановке
gen = accumulator()
next(gen)
gen.send(3)

try:
    gen.send(None)
except StopIteration:
    print("генератор завершился")
Выводсработал break внутри генератора
генератор завершился

То есть None в этом генераторе играет роль условного знака «работа окончена». Есть и прямой способ остановить генератор снаружи — метод close(); о нём и о гарантированной уборке речь пойдёт в следующей главе.

§12Когда send() уместен

send() — инструмент узкого применения, и это нормально. Он нужен там, где генератор должен помнить состояние между порциями данных, а данные приходят не сразу.

main.pyсреднее по мере поступления измерений
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 result
Запуститьnext(gen)
Передатьanswer = gen.send(x)
Равносильноnext(gen) == gen.send(None)
Только приёмreceived = yield

Итог главы

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

Глава 7.3 · Передача значений в генератор · Справочник по Python Автор: Макаров Максим Николаевич