Learning Platform
Глоссарий Troubleshooting
Урок 06.01 · 22 мин
Продвинутый
Iterator protocol__iter____next__StopIterationIterablefor-loop desugarLib/_collections_abc.py

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.pyIterable ABC (~10 lines), Iterator ABC (~15 lines); docs.python.org glossary.

Ключевые наблюдения:

  1. Iterator наследуется от Iterable — каждый итератор тоже iterable (то есть имеет __iter__()).
  2. У Iterator метод __iter__ возвращает self — это и есть знаменитый invariant iter(it) is it.
  3. 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):

  1. Если у type(obj) есть tp_iter slot (т.е. определён __iter__) — вызывает его, ожидает iterator object на выходе.
  2. Иначе если у type(obj) есть tp_subscript (__getitem__) — оборачивает в legacy iterator (sequence iterator), который начинает с obj[0] и инкрементирует индекс до IndexError.
  3. Иначе — 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

MyListiterable, но не 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__.

Iterator vs Iterable — два контракта
Iterabledef __iter__(self): return new iteratorIterable — объект, который умеет создавать новый iterator при каждом вызове iter(). Примеры: list, tuple, dict, str, range. iter(iterable) возвращает СВЕЖИЙ iterator каждый раз — поэтому iterable можно обходить многократно.
iter()
Iteratordef __iter__(self): return selfIterator — объект, поддерживающий __next__. ВАЖНО: __iter__ возвращает self (invariant iter(it) is it). Это ALLOW для for-loop работать с любым iterable ИЛИ iterator одинаково. Примеры: iter([...]), enumerate(...), zip(...), generator object.
next()
value or StopIterationnext(it)next(it) либо возвращает следующее значение, либо raises StopIteration когда последовательность исчерпана. StopIteration — это НЕ ошибка, а control-flow signal: for-loop ловит её и завершается gracefully.

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.

TIP

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: от логики к алгоритмам выполнения

Ключевые выводы

  1. Iterable vs Iterator — два разных контракта. Iterable имеет __iter__(), который возвращает новый iterator. Iterator имеет __next__() плюс __iter__() возвращающий self (invariant iter(it) is it). Все iterators — iterable, не наоборот.
  2. Iterator protocol определён в Lib/_collections_abc.py как Iterable и Iterator ABCs со structural typing через __subclasshook__ — любой класс с правильными методами автоматически считается subclass.
  3. iter(it) is it — invariant, без которого for-loop не работает корректно. Если ваш iterator возвращает «новый объект» из __iter__, цикл может никогда не остановиться (свежий счётчик каждую итерацию).
  4. StopIteration — control flow, не error. for-loop ожидает её и переключает на JUMP через специальный opcode FOR_ITER. Понимание desugar (while True: try: next(); except StopIteration: break) — ключ ко всему остальному в M05.
  5. iter(callable, sentinel) — alternate form для streaming до stop-value (например, iter(lambda: buf.read(1024), b'') читает чанки до EOF).
  6. Re-iterate exhausted iterator → empty. Iterator single-pass; iterable можно итерировать многократно. Используйте itertools.tee для дублирования iterator при необходимости.
  7. Cross-link M03 урок 03: generator expression — это iterator (instance of generator class, который 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.

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

Результат: 0 из 0
Концептуальный
Вопрос 1 из 4. В чём фундаментальное различие между Iterable и Iterator в Python?

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

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

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

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