Iterator protocol: __iter__ и __next__
«Что общего у for x in [1,2,3]:, for line in open('f.txt'):, for k,v in d.items(): и sum(x for x in range(10))?» Все они — iterator protocol. Один и тот же фундаментальный pattern: получить итератор из объекта (через __iter__) и многократно вызывать __next__, пока тот не подаст сигнал StopIteration. CPython построен на этом протоколе насквозь — for-loop в bytecode компилируется в GET_ITER + FOR_ITER, list-конструктор list(iterable) тянет элементы через тот же mechanism, unpacking a, b, *rest = it — тоже.
В этом уроке мы откроем Lib/_collections_abc.py, посмотрим на ABC-определения Iterable и Iterator, разберём invariant iter(it) is it (любой proper iterator должен возвращать сам себя из __iter__), увидим, как for x in iterable: desugars в try/except StopIteration, и узнаем про альтернативную форму iter(callable, sentinel) для streaming-источников. Это база — без неё нельзя двигаться к generator function (M05 урок 02), yield from (урок 03), send/throw/close (урок 04).
Iterable vs Iterator — два разных контракта
Python разводит два связанных, но разных концепта:
- Iterable — объект, способный произвести итератор. Имеет
__iter__()(и/или старый__getitem__()начиная с 0). Примеры:list,tuple,dict,set,str,range, файловые объекты, generator function results. - Iterator — объект, поддерживающий
__next__()(и__iter__(), возвращающий самого себя). Примеры: результатiter([1,2,3]),enumerate(...),zip(...),map(...), generator instances.
Lib/_collections_abc.py фиксирует это формально:
# Lib/_collections_abc.py (упрощённо, сохраняя суть)
class Iterable(metaclass=ABCMeta):
__slots__ = ()
@abstractmethod
def __iter__(self):
while False:
yield None
@classmethod
def __subclasshook__(cls, C):
if cls is Iterable:
return _check_methods(C, "__iter__")
return NotImplemented
class Iterator(Iterable):
__slots__ = ()
@abstractmethod
def __next__(self):
'Return the next item from the iterator. When exhausted, raise StopIteration'
raise StopIteration
def __iter__(self):
return self # <-- KEY invariant!
@classmethod
def __subclasshook__(cls, C):
if cls is Iterator:
return _check_methods(C, '__iter__', '__next__')
return NotImplemented
Cite: Lib/_collections_abc.py — Iterable ABC (~10 lines), Iterator ABC (~15 lines); docs.python.org glossary.
Ключевые наблюдения:
Iteratorнаследуется отIterable— каждый итератор тоже iterable (то есть имеет__iter__()).- У
Iteratorметод__iter__возвращаетself— это и есть знаменитый invariantiter(it) is it. - ABC использует
__subclasshook__для structural typing — любой класс, имеющий нужные методы, считается subclass даже без явногоclass MyIt(Iterator):объявления.
iter(obj) — как Python получает итератор
Когда вы пишете iter(some_iterable), происходит следующее (упрощённая модель _PyObject_GetIter в Objects/iterobject.c / tp_iter slot logic в Objects/typeobject.c):
- Если у
type(obj)естьtp_iterslot (т.е. определён__iter__) — вызывает его, ожидает iterator object на выходе. - Иначе если у
type(obj)естьtp_subscript(__getitem__) — оборачивает в legacy iterator (sequence iterator), который начинает сobj[0]и инкрементирует индекс до IndexError. - Иначе —
TypeError: 'X' object is not iterable.
class MyList:
def __init__(self, items):
self.items = items
def __iter__(self):
return iter(self.items) # delegate к list iterator
for x in MyList([1, 2, 3]):
print(x) # 1 2 3
MyList — iterable, но не iterator: у него нет __next__. iter(MyList(...)) возвращает новый iterator каждый раз. Это позволяет итерировать MyList несколько раз:
ml = MyList([1, 2, 3])
print(list(ml)) # [1, 2, 3]
print(list(ml)) # [1, 2, 3] — снова OK
Сравните с iterator (например, generator или iter([1,2,3])):
it = iter([1, 2, 3])
print(list(it)) # [1, 2, 3]
print(list(it)) # [] — iterator exhausted после первого прохода!
Это fundamental различие. Если хотите многократного обхода — храните iterable; если хотите single-pass streaming — iterator.
Invariant iter(it) is it — ровно self
Каноничный proper iterator определяет __iter__ как return self:
class Counter:
def __init__(self, stop):
self.i = 0
self.stop = stop
def __iter__(self):
return self # <- ключевое!
def __next__(self):
if self.i >= self.stop:
raise StopIteration
x = self.i
self.i += 1
return x
c = Counter(3)
print(iter(c) is c) # True — invariant держится
print(list(c)) # [0, 1, 2]
Зачем этот invariant? Чтобы for x in c: работал корректно даже если c — уже iterator. for-loop сначала вызывает iter(obj), потом next(...) в цикле. Если iter(it) вернул бы новый объект — счётчик внутри c не двигался бы (новый объект имел бы свой self.i = 0), и for-loop крутился бы вечно.
# Контр-пример — нарушение invariant:
class BadCounter:
def __init__(self, stop):
self.i = 0
self.stop = stop
def __iter__(self):
return BadCounter(self.stop) # WRONG — возвращает НОВЫЙ объект
def __next__(self):
if self.i >= self.stop:
raise StopIteration
x = self.i
self.i += 1
return x
bc = BadCounter(3)
for x in bc:
print(x) # печатает 0 бесконечно — каждый iter() возвращает свежий счётчик
if x > 5: break
Lib/_collections_abc.py подсказывает правильный pattern: Iterator.__iter__ уже определён как return self — наследуйте от Iterator и не переопределяйте __iter__.
StopIteration как control-flow signal (не error)
В большинстве языков exceptions — это ошибки. Python использует exceptions расширительно — в том числе как control flow. StopIteration — каноничный пример: это сигнал «больше элементов нет», который iterator кидает при исчерпании, и for-loop ожидает его, ловит и тихо завершается.
it = iter([1, 2])
print(next(it)) # 1
print(next(it)) # 2
print(next(it)) # StopIteration raised — но не как failure, а как "done"
Под капотом for x in it: это:
# Pseudo-code того, во что компилируется for-loop:
_it = iter(it)
while True:
try:
x = next(_it)
except StopIteration:
break # <-- тихо выходим
# ... тело цикла ...
В bytecode это два ключевых opcode’а — GET_ITER (call iter(...)) и FOR_ITER (call next(...) + handle StopIteration). Видно через dis.dis:
import dis
def loop():
for x in [1, 2, 3]:
print(x)
dis.dis(loop)
# ...
# GET_ITER
# >> FOR_ITER to ... # if StopIteration → jump out
# STORE_FAST x
# LOAD_GLOBAL print
# LOAD_FAST x
# CALL # print(x)
# POP_TOP
# JUMP_BACKWARD to FOR_ITER
# ...
FOR_ITER — специальный opcode, который внутри ловит StopIteration и переключает control flow на jump target — без обычного try/except overhead. Это одна из причин, почему idiomatic Python for x in items: быстрее, чем ручной while True: try: next(...) except StopIteration: break — компилятор знает специальный shortcut.
StopIteration изнутри generator function — особый случай (PEP 479). С Python 3.7+ если генератор «случайно» raises StopIteration (например, через next(empty_iter) без обработки), это превращается в RuntimeError: generator raised StopIteration. Раньше это могло молча закрыть outer generator. Тонкость, к которой вернёмся в M05 урок 04.
for-loop desugar — точная семантика
Каноничный for x in iterable: desugars точно так:
# Original:
for x in iterable:
BODY
# Desugar:
_iter = iter(iterable) # 1. Получить iterator (tp_iter slot)
while True:
try:
x = next(_iter) # 2. next() в цикле
except StopIteration:
break # 3. StopIteration → выход
BODY # 4. Тело
# (Optional else clause executes here if loop completed без break)
С else clause:
# Original:
for x in items:
if found(x):
break
else:
print("not found") # else выполняется только если цикл закончился без break
# Desugar:
_iter = iter(items)
_no_break = True
while True:
try:
x = next(_iter)
except StopIteration:
break
if found(x):
_no_break = False
break
if _no_break:
print("not found")
for/else — редко используемый, но мощный pattern. Понимание desugar делает его очевидным: else = «цикл завершился natural-ly через StopIteration», то есть не через break.
iter(callable, sentinel) — alternate form
Менее известная, но полезная форма iter: вместо одного аргумента — два, callable и sentinel value. Возвращает iterator, который при каждом next() вызывает callable; стопает, когда callable возвращает sentinel.
import io
# Чтение блоков по 1024 байт до EOF (b'' = sentinel):
buf = io.BytesIO(b'A' * 5000)
for chunk in iter(lambda: buf.read(1024), b''):
print(len(chunk)) # 1024, 1024, 1024, 1024, 904 (последний — partial)
Эквивалент:
while True:
chunk = buf.read(1024)
if chunk == b'':
break
print(len(chunk))
Идиоматический use case — streaming I/O, где «конец» сигнализируется специальным значением (пустая строка, None, sentinel). См. docs.python.org iter.
Pitfall: re-iterate exhausted iterator
Самый частый footgun протокола — попытка использовать iterator дважды:
it = iter([1, 2, 3])
print(list(it)) # [1, 2, 3]
print(list(it)) # [] — iterator exhausted, повторно итерировать нельзя
Это касается любого iterator: enumerate, zip, map, filter, generator, csv.reader, и т.д. Симптом — second pass возвращает empty (или ничего не делает).
Правильный шаблон, если нужен multi-pass: храните iterable, не iterator. Каждый iter() создаёт fresh iterator:
data = [1, 2, 3] # iterable — list
print(list(data)) # [1, 2, 3]
print(list(data)) # [1, 2, 3] — fine, list is iterable
Или используйте itertools.tee для дублирования:
import itertools
a, b = itertools.tee(iter([1, 2, 3]), 2)
print(list(a)) # [1, 2, 3]
print(list(b)) # [1, 2, 3]
tee буферизует элементы — потребляет память пропорционально расхождению между tee’d iterators (если один опережает другой на K элементов — буфер из K).
Cross-course context
PhysicalPlan: от логики к алгоритмам выполненияКлючевые выводы
- Iterable vs Iterator — два разных контракта. Iterable имеет
__iter__(), который возвращает новый iterator. Iterator имеет__next__()плюс__iter__()возвращающийself(invariantiter(it) is it). Все iterators — iterable, не наоборот. - Iterator protocol определён в
Lib/_collections_abc.pyкакIterableиIteratorABCs со structural typing через__subclasshook__— любой класс с правильными методами автоматически считается subclass. iter(it) is it— invariant, без которогоfor-loop не работает корректно. Если ваш iterator возвращает «новый объект» из__iter__, цикл может никогда не остановиться (свежий счётчик каждую итерацию).- StopIteration — control flow, не error.
for-loop ожидает её и переключает наJUMPчерез специальный opcodeFOR_ITER. Понимание desugar (while True: try: next(); except StopIteration: break) — ключ ко всему остальному в M05. iter(callable, sentinel)— alternate form для streaming до stop-value (например,iter(lambda: buf.read(1024), b'')читает чанки до EOF).- Re-iterate exhausted iterator → empty. Iterator single-pass; iterable можно итерировать многократно. Используйте
itertools.teeдля дублирования iterator при необходимости. - Cross-link M03 урок 03: generator expression — это iterator (instance of
generatorclass, который implements__iter__/__next__). В M05 урок 02 раскроем, что такое generator function на уровне CPython —PyGenObjectс suspended frame.
В уроке M05 урок 02 откроем generator function: def f(): yield x компилируется с флагом CO_GENERATOR и возвращает PyGenObject — суспенднутый stack frame, который resumes через gen_send_ex. Это — самая важная машина в Python для lazy evaluation, async, и iterator pipelines.