yield from — делегирование подгенератору
«Хочу из своего generator выдавать значения другого generator». Простой подход — for x in sub: yield x. Работает для базового случая, но разваливается, как только вы пытаетесь использовать gen.send(...), gen.throw(...), или gen.close() через цепочку. Внешний for-loop не пробрасывает эти операции к sub-generator.
Решение — yield from sub_gen (PEP 380, Python 3.3). Этот синтаксис означает «делегируй всё управление к sub_gen — yields, sends, throws, closes». Это full proxy semantics: outer generator становится прозрачным конвейером для caller и subgenerator. Реализация в Objects/genobject.c — функции gen_send_ex, gen_throw, gen_close следят за gi_yieldfrom указателем и форвардят операции к subgenerator.
В этом уроке: сравним for x in sub: yield x и yield from sub, увидим, в чём разница на уровне send/throw/close, разберём use cases (chain composition, recursive tree-walker, refactor extraction), процитируем gen_throw из CPython source, и реализуем chain(*iters) (этот же pattern в code-challenge).
Базовый пример: yield from desugar
Простейшая форма — пробрасывание yields:
def sub():
yield 1
yield 2
def main():
yield 0
yield from sub() # все yields из sub() уходят caller'у
yield 3
print(list(main())) # [0, 1, 2, 3]
Это выглядит как замена for x in sub(): yield x:
def main_loop():
yield 0
for x in sub():
yield x # ручной forwarding
yield 3
print(list(main_loop())) # [0, 1, 2, 3] — те же значения
Для plain iteration оба варианта эквивалентны. Но yield from делает значительно больше. Detali ниже.
Где for x in sub: yield x ломается
Send forwarding:
def echoer():
while True:
msg = yield
print(f"echoer received: {msg}")
def hand_rolled():
yield from () # primer
for _ in echoer(): # WRONG — нельзя send в plain for-loop
yield # outer yield gets None always
def with_yield_from():
yield from echoer() # CORRECT — sends прокидываются
# С hand-rolled:
g = hand_rolled()
next(g) # выполняется до for-loop, остановлен в for x in echoer()
g.send("hello") # hello уходит в OUTER yield, не в echoer.msg
# С yield from:
g2 = with_yield_from()
next(g2) # выполняется до yield from; resumes echoer; останавливается на echoer's yield
g2.send("hello") # печатает "echoer received: hello"
Throw forwarding:
def sub_with_handler():
try:
yield 1
yield 2
except ValueError as e:
print(f"caught: {e}")
yield 999
def hand_rolled():
for x in sub_with_handler():
yield x
def with_yield_from():
yield from sub_with_handler()
# С hand-rolled:
g = hand_rolled()
next(g) # 1
try:
g.throw(ValueError("boom")) # ValueError propagates через outer for, не в sub
except ValueError:
print("hand_rolled: VE rethrown по for-loop")
# С yield from:
g2 = with_yield_from()
next(g2) # 1
print(g2.throw(ValueError("boom"))) # печатает "caught: boom", возвращает 999
yield from корректно проксирует throw к subgenerator — sub_with_handler ловит исключение и продолжает работать. Hand-rolled for x in sub: yield x пропускает throw мимо subgenerator (исключение убивает outer generator).
Close forwarding:
def sub_with_finally():
try:
yield 1
yield 2
finally:
print("sub teardown")
def hand_rolled():
for x in sub_with_finally():
yield x
def with_yield_from():
yield from sub_with_finally()
# С hand-rolled:
g = hand_rolled()
next(g) # 1
g.close() # нет печати "sub teardown" — close ловится outer for, sub forgotten
# С yield from:
g2 = with_yield_from()
next(g2) # 1
g2.close() # печатает "sub teardown" — close прокинут к sub
yield from гарантирует cleanup в finally блоке subgenerator. Без yield from — потенциальная утечка resource (например, открытый файл, socket).
Cite: PEP 380 — Syntax for Delegating to a Subgenerator. Original motivation именно эти три точки failure.
Full PEP 380 desugar — что на самом деле делает yield from
PEP 380 даёт точный desugar (упрощённо, ~30 строк):
RESULT = yield from EXPR
# desugars (в Python equivalent — реальная реализация в C, Objects/genobject.c):
_i = iter(EXPR)
try:
_y = next(_i)
except StopIteration as _e:
_r = _e.value
else:
while True:
try:
_s = yield _y # forward yield к outer caller
except GeneratorExit as _e:
try:
_m = _i.close
except AttributeError:
pass
else:
_m()
raise _e # re-raise GeneratorExit для cleanup
except BaseException as _e:
_x = sys.exc_info()
try:
_m = _i.throw
except AttributeError:
raise _e
else:
try:
_y = _m(*_x) # forward exception к sub
except StopIteration as _e:
_r = _e.value
break
else:
try:
if _s is None:
_y = next(_i) # plain next forwarding
else:
_y = _i.send(_s) # forward sent value к sub
except StopIteration as _e:
_r = _e.value
break
RESULT = _r
Ключевые моменты:
_y = yield _y— каждый yield из sub становится yield для caller, sent value захватывается.- GeneratorExit (от
outer.close()) проксируется к sub через_i.close(). - Other exceptions (от
outer.throw(...)) проксируются к sub через_i.throw(*sys.exc_info()). - send vs next: если caller делает
outer.send(s), value прокидывается через_i.send(s); еслиouter.next()(i.e.,s is None), →next(_i). - Return value:
StopIteration.valueот sub становится значением outer expressionRESULT = yield from EXPR. Это ключевой механизм для composing generators-as-coroutines.
Cite: PEP 380 (полный desugar в спецификации); Objects/genobject.c — gen_throw (forward exception к delegated subiterator), gen_close (forward GeneratorExit). На уровне C это implemented как state machine, не explicit Python loop.
Implementation — gen_throw и gen_close в Objects/genobject.c
Когда вы вызываете outer.throw(ExcType), и outer сейчас в yield from sub, CPython routes throw к sub. Реализация (упрощённо):
/* Objects/genobject.c — gen_throw, упрощено */
static PyObject *
gen_throw(PyGenObject *gen, PyObject *const *args, Py_ssize_t nargs)
{
PyObject *exc, *value, *tb;
/* parse args в exc/value/tb */
...
/* Проверка: если outer сейчас в yield from, проксируем к sub */
PyObject *yf = gen->gi_yieldfrom; /* указатель на subgenerator */
if (yf != NULL) {
PyObject *ret;
gen->gi_running = 1;
if (PyGen_CheckExact(yf)) {
/* sub — generator: вызываем gen_throw recursively */
ret = gen_throw((PyGenObject *)yf, args, nargs);
} else {
/* sub — обычный iterator: ищем .throw метод и вызываем */
PyObject *meth = _PyObject_LookupAttr(yf, &_Py_ID(throw));
if (meth == NULL) {
/* sub не имеет throw — re-raise в outer */
Py_DECREF(yf);
gen->gi_yieldfrom = NULL;
/* установить exc state, jump к outer's bytecode */
goto throw_here;
}
ret = PyObject_Vectorcall(meth, args, nargs, NULL);
}
gen->gi_running = 0;
return ret;
}
throw_here:
/* outer не в yield from — обычный throw в outer's frame */
return _gen_throw_at_yield_point(gen, exc, value, tb);
}
Главное: gi_yieldfrom — поле PyGenObject, которое указывает на текущий subgenerator во время yield from. Если оно non-NULL, throw/close форвардятся туда; если NULL — обрабатывается в outer frame.
Cite: Objects/genobject.c — gen_throw (~80 LoC), gen_close (~50 LoC); Objects/genobject.c — макрос PyGen_CheckExact. PEP 380 specification (peps.python.org/pep-0380).
Use case 1 — Chain composition
itertools.chain — flatten нескольких iterables в один. Реализация через yield from тривиальна:
def chain(*iters):
for it in iters:
yield from it
print(list(chain([1, 2], [3, 4], [5]))) # [1, 2, 3, 4, 5]
print(list(chain('abc', range(3)))) # ['a', 'b', 'c', 0, 1, 2]
itertools.chain сам имеет C-implementation в Modules/itertoolsmodule.c (~30 LoC), но логика та же. yield from here is the canonical Python expression of chaining — это и delegated __next__ от текущего sub iterator, и automatic переход к next iterator при exhaustion.
Use case 2 — Recursive tree-walker
Yield from + recursion — идеальный pattern для обхода деревьев:
def walk(node):
"""Pre-order traversal: parent before children."""
yield node['value']
for child in node.get('children', []):
yield from walk(child) # recursive delegation
tree = {
'value': 'root',
'children': [
{'value': 'A', 'children': [
{'value': 'A1', 'children': []},
{'value': 'A2', 'children': []},
]},
{'value': 'B', 'children': [
{'value': 'B1', 'children': []},
]},
],
}
print(list(walk(tree)))
# ['root', 'A', 'A1', 'A2', 'B', 'B1']
Без yield from:
def walk_manual(node):
yield node['value']
for child in node.get('children', []):
for v in walk_manual(child): # manually unwrap
yield v
Работает, но ~7 знаков длиннее на каждом уровне рекурсии и не пробрасывает send/throw корректно. С yield from recursion прозрачна — outer caller может делать g.close() в любой момент, и tree-walker корректно cleanups (если в любом узле есть try/finally).
Use case 3 — Refactor extraction
Если у вас длинная generator function — можно извлечь часть в helper и делегировать:
def authenticate_session(user_id):
yield 'auth_request'
token = yield # ждём send'а с токеном
if not validate(token):
return None
return token
def session_loop():
"""Высокоуровневая state machine."""
yield 'starting'
token = yield from authenticate_session('alice') # extract sub-flow
if token is None:
yield 'auth_failed'
return
yield 'authenticated'
yield from process_messages(token) # ещё одна sub-flow
# auth_request → 'token' → 'authenticated' → ...
yield from позволяет разбить длинную coroutine на mini-coroutines, не теряя ни send/throw forwarding, ни return-value-from-StopIteration semantics. Этот pattern был основой asyncio.coroutine (Python 3.4+, до async/await) — именно так строились asynchronous flows до PEP 492.
Capture StopIteration.value — return из subgenerator
Generator может return value (Python 3.3+). Внутри yield from это значение становится значением expression:
def sub():
yield 1
yield 2
return 'sub_result' # → StopIteration('sub_result')
def main():
result = yield from sub() # 'sub_result' captured
yield f"got: {result}"
print(list(main())) # [1, 2, 'got: sub_result']
Без yield from — for x in sub: yield x молча выбрасывает return value. yield from его захватывает и доступит в outer scope.
Это — основа того, как await coro работает в asyncio (legacy @asyncio.coroutine + yield from): coroutine return value доступен в caller через result = yield from coro.
Pitfall — yield from non-iterator argument
yield from EXPR требует, чтобы EXPR был iterable. Если EXPR — non-iterable, получите TypeError:
def gen():
yield from 42 # WRONG — int не iterable
list(gen()) # TypeError: cannot 'yield from' a non-iterator of type 'int'
Также: list/tuple/dict/str работают как iterables, но они НЕ generators. Это означает, что send/throw/close к ним не прокинуты:
def gen():
yield from [1, 2, 3] # OK — list iterable, yields 1, 2, 3
g = gen()
print(next(g)) # 1
g.send('hello') # 2 — sent value игнорируется (list iterator не reagирует на send)
yield from делает best-effort proxy: для не-generators просто iterates обычным next(iter(...)). Send forwarding доступен только когда target — generator (или другой proper coroutine).
Cross-course context
DataFusion: ExecutionPlan compositionCross-course → Storage Formats: 02/01 row-groups —
yield fromделегирует поток values от подгенератора, как чтение Parquet row group делегирует к sequence column chunk readers: верхний reader iterates row groups, а каждый row group composit нескольких column readers под капотом. Та же composition: outer iterator forwardsnext()к inner stream, не зная (и не нуждаясь знать) о его внутренней структуре.
Ключевые выводы
yield from sub— не простоfor x in sub: yield x. Это full PEP 380 proxy: send/throw/close прокидываются к subgenerator корректно. Без этого hand-rolled forwarding теряет sends, mishandles throws, не вызывает sub’stry/finallycleanups.- PEP 380 desugar (~30 строк): yield/send proxy через try/except wrapper; GeneratorExit → sub.close(); BaseException → sub.throw(*exc); StopIteration.value становится значением
yield fromexpression. - CPython implementation:
Objects/genobject.cфункцииgen_throw,gen_closeфорвардят к sub через указательgi_yieldfrom. Еслиgi_yieldfrom != NULL— операция проксируется; иначе — обрабатывается в outer frame. - Use case 1: Chain composition.
def chain(*iters): for it in iters: yield from it— Pythonic flatten нескольких iterables. Эквивалентitertools.chain, но в Python source. - Use case 2: Recursive tree-walker. Yield from идеально для DFS:
yield node; for c in children: yield from walk(c). Без yield from — manually-double-loop, без correct close forwarding. - Use case 3: Refactor extraction. Длинную generator function можно разбить на mini-flows и склеить через yield from —
result = yield from sub_flow(). Так строились asyncio coroutines в Python 3.4-3.5 до async/await. StopIteration.valuecapture —RESULT = yield from sub()присваивает return value sub’а к RESULT. Без yield from значение молча теряется.- Pitfall:
yield from non_iterable— TypeError;yield from [list](non-generator iterable) — OK для plain iteration, но send/throw/close не proxy’ятся (получают best-effort через iter()).
В уроке M05 урок 04 раскроем send / throw / close подробнее: когда gen.send(value) валиден, как gen.throw(ExcT) инжектирует exception в yield expression, что делает gen.close() (и Python 3.13 новость: close() may return final value).