Learning Platform
Глоссарий Troubleshooting
Урок 06.05 · 12 мин
Продвинутый
SummarySynthesisIterator protocolPyGenObjectyield fromsend/throw/closeCross-modulePreview M06

Ключевые выводы по итераторам и генераторам

Этот урок — single-page synthesis Module 05. Если вы прочитали уроки 01-04, эта страница даёт comparative view: пять концепций iteration в Python — iterator protocol как base contract, PyGenObject как саспенднутый frame, yield from как full delegation, send/throw/close как bidirectional control. Используйте перед экзаменом для self-assessment, и для cross-module mental map с M03 / M02 / M06 (предвидение).


Pre-exam consolidation: 5 концепций модуля

#КонцепцияM05 lessonCPython sourceCross-module link
1Iterator protocol = __iter__/__next__/StopIteration + invariant iter(it) is itУрок 01Lib/_collections_abc.py Iterator/Iterable ABCsM02 урок 02 (immutable iteration); M04 урок 02 (__iter__tp_iter slot)
2PyGenObject = compact struct (~176 bytes) с suspended frame; gi_frame + gi_code + gi_running + gi_nameУрок 02Objects/genobject.c gen_send_ex; Include/cpython/genobject.h structM03 урок 03 (gen-expr IS generator function — same PyGenObject)
3CO_GENERATOR flag + YIELD_VALUE opcode = lexical detection в compiler; suspension primitive в bytecodeУрок 02Include/cpython/code.h CO_GENERATOR=0x20; Python/ceval.c TARGET(YIELD_VALUE)n/a (M05 internal)
4yield from = full PEP 380 proxy (forwards send/throw/close, captures StopIteration.value)Урок 03Objects/genobject.c gen_throw/gen_close follow gi_yieldfrom; PEP 380n/a (M05 internal); foundation для async/await (PEP 492)
5send/throw/close = bidirectional communication; primed-generator rule; Python 3.13 close-returnУрок 04Objects/genobject.c gen_send_ex/gen_throw/gen_close; PEP 342M06 урок 04 — @contextmanager (generator wrapped как context manager, yield = boundary)

Cross-module bridges — где iteration встречается с другими модулями

M03 урок 03 → M05 урок 02: gen-expr IS generator function

Generator expression (x*x for x in range(N))синтаксический сахар для anonymous generator function. Compiler буквально создаёт def _anon(): for x in range(N): yield x*x и вызывает её. Возвращает тот же PyGenObject, с тем же co_flags & CO_GENERATOR, тем же memory profile (~200 байт). M03 урок 03 наблюдение «gen-expr ~120 байт vs list 8MB» — за этим стоит та же PyGenObject machinery, что мы разбирали в M05 урок 02.

import sys

# M03 урок 03 — generator expression:
genexp = (i*i for i in range(10**6))
print(sys.getsizeof(genexp))             # ~200 байт

# M05 урок 02 — generator function:
def gen_func():
    for i in range(10**6):
        yield i*i

g = gen_func()
print(sys.getsizeof(g))                  # ~176 байт

# Оба — PyGenObject. Empirically same order of magnitude.
print(type(genexp).__name__, type(g).__name__)   # 'generator' 'generator' — same class!

Cross-link explicit: понимание PyGenObject layout в M05 урок 02 объясняет, почему memory profile из M03 урок 03 ровно такой.

M02 урок 06 → M05 урок 01: immutable elements safe для iteration caching

Iterator protocol даёт single-pass последовательность. Если elements mutable, безопасный snapshot невозможен — следующий next() может вернуть mutated value. Если elements immutable (M02 урок 06 — frozen tuples, strings, ints), последовательность stable: каждый element — frozen value, can be safely cached / hashed / used as dict key.

# Mutable elements — небезопасно:
data = [[1, 2], [3, 4]]
it = iter(data)
first = next(it)
first.append(99)        # ← mutates underlying list element
print(data)             # [[1, 2, 99], [3, 4]] — оригинал тронут!

# Immutable elements — безопасно:
data = [(1, 2), (3, 4)]
it = iter(data)
first = next(it)
# first.append(99)      # AttributeError: tuple has no append
# элементы immutable, iteration safe

Это связка M02 (immutability) + M05 (iteration) для functional pipelinesmap/filter/reduce гарантируют correctness только если intermediate values immutable.

M04 урок 02 → M05 урок 01: __iter__tp_iter slot

В M04 урок 02 мы видели dunder→tp_slot mapping (e.g., __init__tp_init, __hash__tp_hash). Iterator protocol — это два дополнительных slots: tp_iter (для __iter__) и tp_iternext (для __next__). Когда вы пишете for x in obj:, CPython:

  1. Lookup Py_TYPE(obj)->tp_iter — если non-NULL, call it, get iterator.
  2. В loop: lookup Py_TYPE(iter)->tp_iternext — call repeatedly, until raises StopIteration.
# Custom iterable — обоих slots:
class MyRange:
    def __init__(self, n):
        self.n = n

    def __iter__(self):                      # → tp_iter
        return MyRangeIter(self.n)


class MyRangeIter:
    def __init__(self, n):
        self.n = n
        self.i = 0

    def __iter__(self):                      # → tp_iter (returns self for proper iterator)
        return self

    def __next__(self):                      # → tp_iternext
        if self.i >= self.n:
            raise StopIteration
        x = self.i
        self.i += 1
        return x


for x in MyRange(3):
    print(x)        # 0 1 2 — works through tp_iter + tp_iternext slots

Cite: Objects/typeobject.cslot_tp_iter и slot_tp_iternext slot wrappers, генерируемые fixup_slot_dispatchers.

M03 урок 04 → M05 урок 02: lru_cache stores values, not generators

@functools.lru_cache(f) хранит f(args) results в dict. Но если fgenerator function, это даёт subtle behaviour:

from functools import lru_cache

@lru_cache(maxsize=128)
def numbers(n):
    yield from range(n)


g1 = numbers(5)
g2 = numbers(5)
print(g1 is g2)            # True — cached PyGenObject — SAME object!
print(list(g1))            # [0, 1, 2, 3, 4]
print(list(g2))            # [] — exhausted, потому что g2 is g1!

Cache stores PyGenObject, not generator’s results. Second call returns same exhausted generator. Workaround — кешировать tuple(generator(...)):

@lru_cache(maxsize=128)
def numbers_cached(n):
    return tuple(range(n))   # materialize, cache the tuple


print(numbers_cached(5))    # (0, 1, 2, 3, 4)
print(numbers_cached(5))    # (0, 1, 2, 3, 4) — same tuple, fine

Cross-link M02 (hashable) + M03 (lru_cache) + M05 (PyGenObject): cache work если args hashable; cached value — single object; generator caching — pitfall.


Comparative table — pattern recognition

PatternЧто этоM05 lessonCross-module
def f(): yield ...Generator function — compiler ставит CO_GENERATOR flagУрок 02M03 урок 03 (gen-expr)
(expr for x in it)Generator expression — anonymous gen functionM03 урок 03M05 урок 02 (PyGenObject)
iter(obj)Get iterator (tp_iter slot)Урок 01M04 урок 02 (slots)
next(it)Pull next value (tp_iternext slot)Урок 01M04 урок 02 (slots)
for x in obj:Desugar iter + next + StopIteration handlingУрок 01n/a
yield from subFull PEP 380 proxy delegationУрок 03M06 урок 04 preview (@contextmanager)
gen.send(v)Push v как result yield expressionУрок 04n/a
gen.throw(E)Inject exception в yield pointУрок 04n/a
gen.close()Inject GeneratorExit для cleanup; Python 3.13 returns finalУрок 04M06 урок 04 (__exit__ analog)

Preview M06 — educational climax: @contextmanager wraps generator

В M06 (Decorators & Context Managers) увидим synthesis Phase 66:

from contextlib import contextmanager

@contextmanager
def open_resource():
    print("setup")
    try:
        yield 42                          # yield = boundary __enter__/__exit__
    finally:
        print("teardown")


with open_resource() as value:
    print(f"using: {value}")              # using: 42

# Output:
# setup
# using: 42
# teardown

Что здесь происходит? @contextmanager декоратор оборачивает generator function в _GeneratorContextManager. Внутри:

  • __enter__() вызывает next(generator) — runs everything до yield, returns yielded value.
  • __exit__(exc_type, exc, tb) — если exception, gen.throw(exc_type, exc, tb); иначе next(gen) для completion. try/finally блок в generator runs — это и есть teardown.

Cross-Phase 66 climax:

  • M03 урок 04 (closure) дал контекст для понимания decorator.
  • M05 урок 02 (PyGenObject) дал структуру generator с suspended frame.
  • M05 урок 04 (send/throw/close) дал mechanism для bidirectional communication, особенно gen.throw(...) который __exit__ использует для exception propagation.
  • M06 урок 04 объединит всё это: closure + generator + yield = context manager protocol.

yield в @contextmanager-decorated function = boundary: всё до yield = __enter__, всё после = __exit__. Это самая elegant abstraction в Python — три mechanisms (closure, generator, context manager) встречаются в одной функции.

Cite forward: Lib/contextlib.py _GeneratorContextManager. Ждём в M06 урок 04.


Self-assessment перед экзаменом

Чек-лист (7 пунктов). Проверьте каждое утверждение для себя — если не уверены, перечитайте указанный урок:

  • Я могу описать iterator protocol invariant iter(it) is it и объяснить, почему он critical для for-loop. (M05 урок 01)
  • Я могу перечислить 5 ключевых полей PyGenObject (gi_frame, gi_code, gi_running, gi_name, gi_qualname) и сказать, что хранит каждый. (M05 урок 02)
  • Я могу сказать, что вернёт def gen(): return 42 (без yield). Ответ: НЕ generator — обычный return 42 (нет CO_GENERATOR flag). (M05 урок 02)
  • Я могу привести use case yield from (chain composition / tree-walker / refactor extraction) и объяснить, чем он лучше hand-rolled for x in sub: yield x. (M05 урок 03)
  • Я могу различить gen.send(v) / gen.throw(E) / gen.close() по semantics: send pushes value to yield expression; throw injects exception; close injects GeneratorExit. (M05 урок 04)
  • Я могу empirically prove, что (i*i for i in range(10**6)) ≈ 200 байт, а [i*i for i in range(10**6)] ≈ 8 MB через sys.getsizeof. (M05 урок 02)
  • Я могу спрогнозировать, что @contextlib.contextmanager-decorated generator function делает с yield: yield = boundary между __enter__ (всё до yield) и __exit__ (всё после yield, включая finally). (M05 урок 05 preview → M06 урок 04)

Cross-course context

PhysicalPlan: pull-based выполнение запросов

Что в M06 — Decorators и Context Managers

Module 06 (Decorators & Context Managers) — продолжает M03 (closures) и M05 (generators) для applied patterns:

  • Function decorator = closure, оборачивающий callable (M06 урок 01 — revisits M03 урок 04 closure cells)
  • Class decorator = __init__ + __call__ makes instance callable (M06 урок 02)
  • @functools.wraps копирует metadata к wrapper (M06 урок 03)
  • with statement = __enter__ / __exit__ protocol (M06 урок 04)
  • @contextlib.contextmanager = generator-based context manager (M06 урок 05) — climax synthesis closure + generator + context manager

Это practical часть Phase 66: M04+M05 заложили fundamental abstractions; M06 их применит к real-world patterns (timing decorator, retry, suppress exceptions, redirect_stdout, ExitStack для multiple resources).

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

Результат: 0 из 0
Прикладной
Вопрос 1 из 4. Cross-link M03 → M05: что общего у gen-expr `(x*x for x in range(N))` (M03 урок 03) и `def gen(): for x in range(N): yield x*x` (M05 урок 02)?

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

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

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

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