PythonMiddleTechnical
Чем yield отличается от yield from?
yield from делегирует итерацию другому итерируемому объекту или генератору, прозрачно пробрасывая send(), throw() и return-значение. Это чище, чем цикл for + yield, и обязательно для корутин на генераторах.
yield from: делегирование генераторов
yield from iterable, появившийся в Python 3.3 (PEP 380), делает три вещи одновременно:
- Передаёт все значения из
iterableвызывающему коду. - Прозрачно пробрасывает
send()иthrow()во вложенный генератор. - Получает возвращаемое значение (
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 хотя бы концептуально.