PythonMiddleTechnical

Чем yield отличается от yield from?

yield from делегирует итерацию другому итерируемому объекту или генератору, прозрачно пробрасывая send(), throw() и return-значение. Это чище, чем цикл for + yield, и обязательно для корутин на генераторах.

yield from: делегирование генераторов

yield from iterable, появившийся в Python 3.3 (PEP 380), делает три вещи одновременно:

  1. Передаёт все значения из iterable вызывающему коду.
  2. Прозрачно пробрасывает send() и throw() во вложенный генератор.
  3. Получает возвращаемое значение (return) вложенного генератора как результат выражения yield from.

Разница: for+yield vs yield from

from typing import Generator, Iterable


# Старый способ — не пробрасывает send() и throw()
def chain_old(*iterables: Iterable) -> Generator:
    for it in iterables:
        for item in it:
            yield item


# Новый способ с yield from
def chain_new(*iterables: Iterable) -> Generator:
    for it in iterables:
        yield from it


result = list(chain_new([1, 2], [3, 4], [5]))
print(result)  # [1, 2, 3, 4, 5]

Пробрасывание return-значения

def inner() -> Generator[int, None, str]:
    yield 1
    yield 2
    return "inner done"  # возвращается через StopIteration.value


def outer() -> Generator[int, None, str]:
    result = yield from inner()  # result = "inner done"
    print(f"Inner returned: {result}")
    return "outer done"


gen = outer()
print(next(gen))   # 1
print(next(gen))   # 2
try:
    next(gen)      # Inner returned: inner done → StopIteration
except StopIteration as e:
    print(e.value) # outer done

Пробрасывание send() и throw()

def inner_accumulator() -> Generator[float, float, None]:
    total = 0.0
    while True:
        value = yield total
        if value is None:
            return
        total += value


def outer_wrapper() -> Generator[float, float, None]:
    print("Starting")
    yield from inner_accumulator()  # send() идёт прямо в inner
    print("Done")


gen = outer_wrapper()
next(gen)       # Starting → 0.0
gen.send(10)    # → 10.0  (send ушёл в inner_accumulator)
gen.send(5)     # → 15.0

Рекурсивный обход дерева

from typing import Any


def flatten(nested: Any) -> Generator:
    """Рекурсивно разворачивает вложенные списки."""
    if isinstance(nested, list):
        for item in nested:
            yield from flatten(item)  # рекурсия через yield from
    else:
        yield nested


data = [1, [2, [3, 4]], [5, 6]]
print(list(flatten(data)))  # [1, 2, 3, 4, 5, 6]

Историческая роль: корутины до asyncio

До появления async/await (Python 3.5) корутины реализовывались через генераторы с yield from. Декоратор @asyncio.coroutine и yield from asyncio.sleep(1) — предшественники современного await. Сегодня это legacy, но понимание помогает читать старый код.

Подводные камни

  • yield from работает только с итерируемыми объектами или генераторами; yield from None вызовет TypeError.
  • При использовании с обычным списком yield from [1, 2, 3] — значения пробрасываются, но send() туда не передаётся (список не поддерживает протокол генератора).
  • Смешивание yield и yield from в одной функции работает, но усложняет понимание потока управления.
  • Возвращаемое значение yield from доступно только внутри генератора; снаружи оно теряется в StopIteration.value и игнорируется обычным for.
  • В async-коде yield from не работает — используйте await и async for.
  • Рекурсивный yield from на глубоко вложенных структурах создаёт длинную цепочку генераторов — возможна RecursionError при очень большой глубине.
  • Отладка сложнее: трейсбек проходит через несколько уровней генераторов, что затрудняет поиск источника ошибки.

Common mistakes

  • Не знать про return value вложенного генератора.
  • Использовать yield from для одного значения.
  • Не понимать делегирование исключений.

What the interviewer is testing

  • Показывает flatten.
  • Объясняет делегирование генератору.
  • Знает про send/throw/close хотя бы концептуально.

Sources

Related topics