send / throw / close — двусторонняя коммуникация
В уроках 02-03 мы видели одностороннюю коммуникацию — generator yield-ит, caller next-ит и потребляет. Но yield это expression, а не statement. У него есть значение — то, что было послано через gen.send(value). Это превращает generator из «producer of values» в двусторонний канал: caller отправляет данные внутрь, generator отвечает.
Кроме send, есть ещё две операции:
gen.throw(ExcType)— инжектирует исключение в текущую точку yield внутри generator. Generator может catch её черезtry/exceptи продолжить, или дать exception распространиться.gen.close()— посылает specific исключениеGeneratorExitвнутрь generator. Это запускает всеtry/finallyблоки для корректного cleanup. С Python 3.13 —close()may return final value (раньше всегдаNone).
В этом уроке разберём каждую операцию: gen.send mechanics + primed-generator rule (нельзя send non-None в fresh generator), gen.throw semantics, gen.close finally-blocks cleanup, и упомянем PEP 342 (generators-as-coroutines, foundation для async/await — full coverage в следующем milestone). Откроем gen_send_ex, gen_throw, gen_close в Objects/genobject.c.
gen.send(value) — value захватывается в yield expression
yield — это expression с возвращаемым значением:
def echo():
while True:
msg = yield # yield на правой стороне = expression
print(f"got: {msg}")
g = echo()
next(g) # первый next — primer (см. ниже)
g.send("hello") # got: hello — sent value присвоен msg
g.send("world") # got: world
g.close() # завершаем generator
Mechanics: при g.send(value), CPython вызывает gen_send_ex(gen, value, ...). Этот function (см. M05 урок 02):
- Проверяет, что generator не уже-running.
- Pushes
valueна evaluation stack — это и есть значение, которое получитyieldexpression при resume. - Resumes frame через
_PyEval_EvalFrameEx. - Bytecode interpreter внутри
YIELD_VALUEopcode handler делаетvalue = POP()— value становится результатом yield expression.
def show_yield_value():
print("first yield...")
x = yield 1
print(f"received x = {x}")
print("second yield...")
y = yield 2
print(f"received y = {y}")
return "done"
g = show_yield_value()
print(next(g)) # first yield... → 1
# yield 1 expression now ждёт send/next:
print(g.send("A")) # received x = A → second yield... → 2
print(g.send("B")) # received y = B → return done → StopIteration("done")
next(g) ≡ g.send(None) — это same operation. next(g) буквально вызывает gen_send_ex(gen, Py_None, ...). Поэтому for x in gen: это просто loop вокруг gen.send(None).
Cite: Objects/genobject.c gen_send_ex (~150 LoC) — central function для всех operations; pushed value становится yield expression result через *f_stacktop++ = arg в начале функции.
Primed-generator rule
Pitfall: нельзя send(non_None_value) в fresh (только-что-созданный) generator:
def gen():
x = yield 1
g = gen()
g.send("hello")
# TypeError: can't send non-None value to a just-started generator
Зачем это правило? На самом первом resume, frame только что allocated — нет yield expression, который мог бы захватить sent value. Если allow это, value просто потеряется. Python explicit ловит этот случай и raises TypeError.
Workaround — prime generator через next(g) (или g.send(None)) сначала:
def coroutine():
while True:
msg = yield
print(f"received: {msg}")
g = coroutine()
next(g) # PRIMER — двигаем generator до первого yield, чтобы было куда send'ить
g.send("hello") # received: hello
Cite в gen_send_ex:
/* Objects/genobject.c — primed-generator check */
if (f->f_lasti == -1 && arg != Py_None) {
PyErr_SetString(PyExc_TypeError,
"can't send non-None value to a just-started generator");
return NULL;
}
f->f_lasti == -1 означает «никогда не resumed» — generator всё ещё на самом старте. Combined с arg != Py_None — это попытка прислать значение, которое некому будет принять.
Можно автоматизировать примирование декоратором (PEP 342 era):
def coroutine_decorator(func):
"""Auto-prime generator при создании."""
def wrapper(*args, **kwargs):
gen = func(*args, **kwargs)
next(gen)
return gen
return wrapper
@coroutine_decorator
def echoer():
while True:
msg = yield
print(f"echo: {msg}")
g = echoer() # primed автоматически
g.send("hi") # echo: hi — без явного next() не нужен
Это была идиома до асинхронного Python.
gen.throw(ExcType) — exception injection
gen.throw(ExcType) инжектирует исключение в текущую точку yield generator’а. Если generator имеет try/except вокруг yield — он может catch её:
def resilient():
try:
yield 1
except ValueError as e:
print(f"caught VE: {e}")
yield 'recovered'
yield 'end'
g = resilient()
print(next(g)) # 1
print(g.throw(ValueError("boom"))) # caught VE: boom → 'recovered'
print(next(g)) # 'end'
Если generator не ловит exception, она propagates через generator к caller’у:
def fragile():
yield 1
yield 2
g = fragile()
print(next(g)) # 1
try:
g.throw(ValueError("boom"))
except ValueError as e:
print(f"caller caught: {e}") # caller caught: boom
После uncaught exception, generator закрыт — последующие next raise StopIteration.
Cite: Objects/genobject.c — gen_throw(...) (~80 LoC). Функция:
- Проверяет
gi_yieldfrom— если не NULL, проксирует к sub (см. урок 03 yield from). - Иначе устанавливает exception state в thread state.
- Resumes frame через
_PyEval_EvalFrameEx— interpreter получает exception вYIELD_VALUEopcode (вместо value). - Frame обрабатывает exception обычным way (try/except blocks); если не caught — propagates наружу.
Использование throw — для signaling generator’у что-то вне обычного yield protocol: «request cancellation», «inject test failure», «invalidate cache». До asyncio это была единственная форма bidirectional control.
gen.close() — graceful shutdown через GeneratorExit
gen.close() — это специальный gen.throw(GeneratorExit). Семантика:
- Inject
GeneratorExitв текущую yield point. - Если generator catches её через
except GeneratorExit:— OK, может do cleanup, но должен return / re-raise. Если онyieldпосле catch’а — RuntimeError “generator ignored GeneratorExit”. - Если generator не catches её —
try/finallyблоки run для cleanup. - После cleanup — frame deallocated, generator закрыт.
def with_cleanup():
try:
yield 1
yield 2
finally:
print("cleanup!")
g = with_cleanup()
print(next(g)) # 1
g.close() # cleanup! — finally block executes
print(next(g)) # StopIteration — generator closed
finally в generator — главный механизм для resource management:
def reading_file(path):
f = open(path)
try:
for line in f:
yield line.rstrip()
finally:
f.close() # выполнится при close() или exhaustion
# Если caller прерывает iteration:
g = reading_file('data.txt')
for line in g:
if line.startswith('END'):
break # ← break в for-loop вызывает g.close() через GC eventually
# (или явный g.close() для immediate cleanup)
Cite: Objects/genobject.c — gen_close(...) (~50 LoC). Функция:
- Если
gi_yieldfromnon-NULL — проксирует к sub’s close (yield from корректно forwards). - Раises
GeneratorExitчерезgen_throw(gen, GeneratorExit, NULL, NULL). - После resume:
- Если generator returned (StopIteration) — OK.
- Если raised GeneratorExit или StopIteration — OK (graceful).
- Если raised что-то другое — propagates к caller (e.g., если finally block raises).
- Если generator продолжил yield’ить после catch’а — RuntimeError “generator ignored GeneratorExit”.
def bad_handler():
try:
yield 1
except GeneratorExit:
yield 2 # ← BAD: yield после GeneratorExit catch
# Python raises RuntimeError: generator ignored GeneratorExit
g = bad_handler()
next(g)
g.close()
# RuntimeError: generator ignored GeneratorExit
Python 3.13: gen.close() may return final value
Recent semantic (Python 3.13, 2024): gen.close() теперь может вернуть значение, если generator handles GeneratorExit и returns explicitly. Раньше close() всегда returns None.
# Python 3.13+:
def gen():
try:
yield 1
yield 2
except GeneratorExit:
return "cleaned" # generator caught, returned
g = gen()
next(g) # 1
result = g.close()
# Python 3.13: result == "cleaned"
# Python 3.12 и раньше: result == None (return value silently discarded)
Это позволяет caller получать final value от generator при graceful shutdown — useful для finalize-and-return patterns в coroutines. Реализация: gen_close в Python 3.13 пробрасывает StopIteration.value наружу как return value close().
В Python 3.12 и раньше close() всегда returns None независимо от того, что generator handle сделает. Если ваш код должен работать с обеими версиями — не полагайтесь на этот return value. Проверка: sys.version_info >= (3, 13).
PEP 342 — generators-as-coroutines (forward-link к async)
PEP 342 (Python 2.5, 2005) добавил send/throw/close к generators именно чтобы превратить их в coroutines — двусторонние единицы computation, которые можно чередовать. Идея: yield = «pause», send = «resume with value». Frameworks типа Tornado, Twisted строили асинхронность поверх этого.
# Pre-async/await coroutine pattern (legacy):
def fetch_url(url):
response = yield make_request(url) # await ИЛИ coroutine yield
data = yield parse_response(response)
return data
PEP 525 (Python 3.6, 2016) пошёл дальше — ввёл async generators через async def f(): yield x. Это новая структура (PyAsyncGenObject вместо PyGenObject), но фундаментально те же mechanics — frame suspension via async yield, resume via anext.
Forward link — full async coverage будет в Phase 70 / v2 milestone (см. ADVN-01). PEP 342 показал, что generators могут служить как coroutines через send() (yield boundary как proto-await). PEP 525 ввёл async generators (async def + yield). В этом курсе мы остаёмся на synchronous generators — async/await требует отдельного пути в asyncio event loop, что pyodide ограничен и заслуживает full milestone.
Pitfall — return value inside generator
Что вернёт for x in gen(): если generator имеет return value?
def gen_with_return():
yield 1
yield 2
return "final" # становится StopIteration("final")
for x in gen_with_return():
print(x) # 1, 2 — final НЕ напечатается
# return value молча выбрасывается!
Внутри generator function return value — это raise StopIteration(value) (PEP 479). Caller’ы видят StopIteration, ловят её в for-loop / list() и тихо завершают. Только два способа capture’ить return value:
# 1. yield from (M05 урок 03):
def main():
result = yield from gen_with_return() # 'final' — captured
print(f"got: {result}")
# 2. Manual next() loop с try/except:
g = gen_with_return()
try:
while True:
next(g)
except StopIteration as e:
print(f"got: {e.value}") # got: final
# 3. Python 3.13+ close() с handler:
def gen_close_return():
try:
yield 1
except GeneratorExit:
return "from close"
g = gen_close_return()
next(g)
print(g.close()) # Python 3.13: 'from close'; ранее: None
Cite: PEP 479 — Change StopIteration handling inside generators (Python 3.7+ автоматически converts случайные StopIteration в RuntimeError); Objects/genobject.c — функция gen_send_ex обрабатывает return как StopIteration injection.
Cross-course context
PhysicalPlan: операторы и жизненный цикл выполненияКлючевые выводы
gen.send(value)— отправляет value в generator; value становится результатом yield expression при resume.next(gen)≡gen.send(None). Push value на frame’s evaluation stack происходит вgen_send_ex(Objects/genobject.c).- Primed-generator rule: нельзя
send(non_None)в fresh generator (f_lasti == -1). Workaround:next(gen)сначала или auto-prime decorator.gen_send_exявно проверяет это и raises TypeError. gen.throw(ExcType)— инжектирует exception в текущий yield point. Generator может catch черезtry/exceptили дать propagate. Implementation:gen_throwв Objects/genobject.c, проксирует к sub еслиgi_yieldfrom!= NULL (yield from chain).gen.close()— посылает GeneratorExit внутрь generator, запускаетtry/finallycleanup. Implementation:gen_close(Objects/genobject.c) raises GeneratorExit через gen_throw. Generator должен либо not catch GeneratorExit, либо catch + return / re-raise. Yield после catch GeneratorExit → RuntimeError.- Python 3.13 — close() may return final value. Раньше
close()всегда returns None; с 3.13 если generator catches GeneratorExit и returns, value доступно как return от close(). Полезно для finalize-and-return patterns. finallyв generator — главный механизм resource management. Files / sockets / locks открытые в generator должны иметь try/finally close — иначе утечка при caller’s break / close().return valueвнутри generator — этоraise StopIteration(value)(PEP 479).for/list()молча discardят. Capture:result = yield from gen()или manualtry/except StopIteration as e: e.value.- Forward link к async (per A3 / phase context — 2 sentences only): PEP 342 показал что generators могут быть coroutines через send (yield = proto-await). PEP 525 ввёл async generators (
async def+yield). Полное освещение асинхронности — v2 milestone (ADVN-01).
В уроке M05 урок 05 сделаем pre-exam consolidation: 5 концепций модуля (iterator protocol, PyGenObject, yield from, send/throw/close), cross-link с M03 (gen-expr), M02 (immutable iteration), и preview M06 (@contextmanager оборачивает generator — это educational climax Phase 66).