Ключевые выводы по итераторам и генераторам
Этот урок — 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 lesson | CPython source | Cross-module link |
|---|---|---|---|---|
| 1 | Iterator protocol = __iter__/__next__/StopIteration + invariant iter(it) is it | Урок 01 | Lib/_collections_abc.py Iterator/Iterable ABCs | M02 урок 02 (immutable iteration); M04 урок 02 (__iter__ → tp_iter slot) |
| 2 | PyGenObject = compact struct (~176 bytes) с suspended frame; gi_frame + gi_code + gi_running + gi_name | Урок 02 | Objects/genobject.c gen_send_ex; Include/cpython/genobject.h struct | M03 урок 03 (gen-expr IS generator function — same PyGenObject) |
| 3 | CO_GENERATOR flag + YIELD_VALUE opcode = lexical detection в compiler; suspension primitive в bytecode | Урок 02 | Include/cpython/code.h CO_GENERATOR=0x20; Python/ceval.c TARGET(YIELD_VALUE) | n/a (M05 internal) |
| 4 | yield from = full PEP 380 proxy (forwards send/throw/close, captures StopIteration.value) | Урок 03 | Objects/genobject.c gen_throw/gen_close follow gi_yieldfrom; PEP 380 | n/a (M05 internal); foundation для async/await (PEP 492) |
| 5 | send/throw/close = bidirectional communication; primed-generator rule; Python 3.13 close-return | Урок 04 | Objects/genobject.c gen_send_ex/gen_throw/gen_close; PEP 342 | M06 урок 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 pipelines — map/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:
- Lookup
Py_TYPE(obj)->tp_iter— если non-NULL, call it, get iterator. - В 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.c — slot_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. Но если f — generator 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 lesson | Cross-module |
|---|---|---|---|
def f(): yield ... | Generator function — compiler ставит CO_GENERATOR flag | Урок 02 | M03 урок 03 (gen-expr) |
(expr for x in it) | Generator expression — anonymous gen function | M03 урок 03 | M05 урок 02 (PyGenObject) |
iter(obj) | Get iterator (tp_iter slot) | Урок 01 | M04 урок 02 (slots) |
next(it) | Pull next value (tp_iternext slot) | Урок 01 | M04 урок 02 (slots) |
for x in obj: | Desugar iter + next + StopIteration handling | Урок 01 | n/a |
yield from sub | Full PEP 380 proxy delegation | Урок 03 | M06 урок 04 preview (@contextmanager) |
gen.send(v) | Push v как result yield expression | Урок 04 | n/a |
gen.throw(E) | Inject exception в yield point | Урок 04 | n/a |
gen.close() | Inject GeneratorExit для cleanup; Python 3.13 returns final | Урок 04 | M06 урок 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-rolledfor 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)withstatement =__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).