Learning Platform
Глоссарий Troubleshooting
Урок 06.04 · 25 мин
Продвинутый
sendthrowcloseGeneratorExitPrimed generatorCoroutines (PEP 342)Python 3.13 close returnBidirectional communication

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):

  1. Проверяет, что generator не уже-running.
  2. Pushes value на evaluation stack — это и есть значение, которое получит yield expression при resume.
  3. Resumes frame через _PyEval_EvalFrameEx.
  4. Bytecode interpreter внутри YIELD_VALUE opcode 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.cgen_throw(...) (~80 LoC). Функция:

  1. Проверяет gi_yieldfrom — если не NULL, проксирует к sub (см. урок 03 yield from).
  2. Иначе устанавливает exception state в thread state.
  3. Resumes frame через _PyEval_EvalFrameEx — interpreter получает exception в YIELD_VALUE opcode (вместо value).
  4. 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). Семантика:

  1. Inject GeneratorExit в текущую yield point.
  2. Если generator catches её через except GeneratorExit:OK, может do cleanup, но должен return / re-raise. Если он yield после catch’а — RuntimeError “generator ignored GeneratorExit”.
  3. Если generator не catches её — try/finally блоки run для cleanup.
  4. После 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.cgen_close(...) (~50 LoC). Функция:

  1. Если gi_yieldfrom non-NULL — проксирует к sub’s close (yield from корректно forwards).
  2. Раises GeneratorExit через gen_throw(gen, GeneratorExit, NULL, NULL).
  3. После 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().

NOTE

В Python 3.12 и раньше close() всегда returns None независимо от того, что generator handle сделает. Если ваш код должен работать с обеими версиями — не полагайтесь на этот return value. Проверка: sys.version_info >= (3, 13).


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.

TIP

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: операторы и жизненный цикл выполнения

Ключевые выводы

  1. 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).
  2. Primed-generator rule: нельзя send(non_None) в fresh generator (f_lasti == -1). Workaround: next(gen) сначала или auto-prime decorator. gen_send_ex явно проверяет это и raises TypeError.
  3. gen.throw(ExcType) — инжектирует exception в текущий yield point. Generator может catch через try/except или дать propagate. Implementation: gen_throw в Objects/genobject.c, проксирует к sub если gi_yieldfrom != NULL (yield from chain).
  4. gen.close() — посылает GeneratorExit внутрь generator, запускает try/finally cleanup. Implementation: gen_close (Objects/genobject.c) raises GeneratorExit через gen_throw. Generator должен либо not catch GeneratorExit, либо catch + return / re-raise. Yield после catch GeneratorExit → RuntimeError.
  5. Python 3.13 — close() may return final value. Раньше close() всегда returns None; с 3.13 если generator catches GeneratorExit и returns, value доступно как return от close(). Полезно для finalize-and-return patterns.
  6. finally в generator — главный механизм resource management. Files / sockets / locks открытые в generator должны иметь try/finally close — иначе утечка при caller’s break / close().
  7. return value внутри generator — это raise StopIteration(value) (PEP 479). for/list() молча discardят. Capture: result = yield from gen() или manual try/except StopIteration as e: e.value.
  8. 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).

Проверьте понимание

Результат: 0 из 0
Прикладной
Вопрос 1 из 4. Что произойдёт при `g.send('hello')` для fresh (только-что-созданного, не-primed) generator?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 5