Learning Platform
Глоссарий Troubleshooting
Урок 06.03 · 22 мин
Продвинутый
yield fromPEP 380DelegationProxy semanticsChain compositionTree traversalgen_throwgen_close

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

Ключевые моменты:

  1. _y = yield _y — каждый yield из sub становится yield для caller, sent value захватывается.
  2. GeneratorExit (от outer.close()) проксируется к sub через _i.close().
  3. Other exceptions (от outer.throw(...)) проксируются к sub через _i.throw(*sys.exc_info()).
  4. send vs next: если caller делает outer.send(s), value прокидывается через _i.send(s); если outer.next() (i.e., s is None), → next(_i).
  5. Return value: StopIteration.value от sub становится значением outer expression RESULT = yield from EXPR. Это ключевой механизм для composing generators-as-coroutines.

Cite: PEP 380 (полный desugar в спецификации); Objects/genobject.cgen_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.cgen_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 fromfor 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.

yield from delegation: caller ↔ outer ↔ sub
caller
outer (yield from)
sub (subgenerator)
next(outer)next(sub)y (yielded)y (forwarded)outer.send(s)sub.send(s)outer.throw(ExcT)sub.throw(ExcT)StopIteration(retval)

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 composition

Cross-course → Storage Formats: 02/01 row-groupsyield from делегирует поток values от подгенератора, как чтение Parquet row group делегирует к sequence column chunk readers: верхний reader iterates row groups, а каждый row group composit нескольких column readers под капотом. Та же composition: outer iterator forwards next() к inner stream, не зная (и не нуждаясь знать) о его внутренней структуре.


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

  1. yield from sub — не просто for x in sub: yield x. Это full PEP 380 proxy: send/throw/close прокидываются к subgenerator корректно. Без этого hand-rolled forwarding теряет sends, mishandles throws, не вызывает sub’s try/finally cleanups.
  2. PEP 380 desugar (~30 строк): yield/send proxy через try/except wrapper; GeneratorExit → sub.close(); BaseException → sub.throw(*exc); StopIteration.value становится значением yield from expression.
  3. CPython implementation: Objects/genobject.c функции gen_throw, gen_close форвардят к sub через указатель gi_yieldfrom. Если gi_yieldfrom != NULL — операция проксируется; иначе — обрабатывается в outer frame.
  4. Use case 1: Chain composition. def chain(*iters): for it in iters: yield from it — Pythonic flatten нескольких iterables. Эквивалент itertools.chain, но в Python source.
  5. 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.
  6. 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.
  7. StopIteration.value captureRESULT = yield from sub() присваивает return value sub’а к RESULT. Без yield from значение молча теряется.
  8. 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).

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

Результат: 0 из 0
Концептуальный
Вопрос 1 из 5. Что фундаментально вводит PEP 380 (Python 3.3, `yield from` syntax)?

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

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

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

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