Зачем протокол Python требует, чтобы вызов iter у самого итератора возвращал тот же объект?

Зачем протокол Python требует, чтобы вызов iter() у самого итератора возвращал тот же объект?

Проходите собеседования с ИИ помощником Hintsage

Краткий ответ

Это гарантирует, что итератор сохраняет единое состояние обхода независимо от того, получен он напрямую или через iter(). Поэтому iter(итератор) должен вернуть этот же объект, а не создать новый независимый обходчик.

Благодаря этому цикл for, вложенные потребители и функции, принимающие итерируемые объекты, работают с предсказуемым состоянием итерации.

Исторический контекст

Единый протокол итерации нужен для того, чтобы разные коллекции и потоковые источники можно было обходить одинаково — через получение итератора и последовательные вызовы next().

Разделение итерируемого объекта и итератора решает две разные задачи: коллекция может порождать новые независимые обходы, а итератор представляет конкретный прогресс одного обхода.

Постановка проблемы

Если объект, уже являющийся итератором, при каждом вызове iter() создаёт новый объект, несколько участников могут неожиданно получить разные состояния обхода. Такой объект может внешне работать в простом цикле, но нарушает контракт итератора и становится несовместимым с кодом, который рассчитывает на единое состояние.

Особенно опасны вложенные обходы, передача объекта в несколько функций и повторное получение итератора внутри алгоритма. Ошибка проявляется не обязательно сразу: последовательность элементов может дублироваться, начинаться заново или расходиться между потребителями.

Подробное решение

Для корректного итератора выполняется инвариант: iter(x) is x. Метод __iter__() возвращает сам итератор, а метод __next__() изменяет его внутреннее состояние и либо выдаёт следующий элемент, либо завершает обход исключением StopIteration.

class Counter: def __init__(self, limit): self.current = 0 self.limit = limit def __iter__(self): return self def __next__(self): if self.current >= self.limit: raise StopIteration value = self.current self.current += 1 return value iterator = Counter(2) print(iter(iterator) is iterator) # True print(next(iter(iterator))) # 0 print(next(iterator)) # 1

Вызов next(iter(iterator)) не создаёт новый обход: iter(iterator) возвращает тот же объект, поэтому счётчик продолжает изменяться в общем состоянии.

Это не означает, что любой итерируемый объект должен возвращать себя. Например, список является итерируемым объектом, но не итератором: каждый вызов iter(список) создаёт новый итератор с отдельной позицией. Требование iter(x) is x относится именно к объектам, которые сами реализуют итераторный протокол.

Практический компромисс таков: объект-источник данных обычно делают повторно итерируемым, а отдельный объект обхода — одноразовым итератором. Если нужен новый независимый проход, следует снова вызвать iter() у источника, а не пытаться перезапустить уже используемый итератор.

Ситуация из практики

Предположим, поток токенов передаётся в функцию синтаксического анализа. Функция несколько раз вызывает iter() для проверки lookahead и затем продолжает чтение. Если потоковый объект возвращает новый итератор вместо себя, проверки могут читать данные из одного состояния, а основной разбор — из другого. В результате токены пропускаются или обрабатываются повторно.

Возможны два решения. Можно возвращать новый итератор при каждом вызове: это удобно для повторных обходов, но требует, чтобы исходный объект был именно фабрикой итераторов и хранил данные отдельно от состояния обхода. Либо можно реализовать объект как один итератор, возвращающий себя: состояние единообразно, но повторный независимый проход невозможен без создания нового объекта.

Для потокового парсера выбирают второй вариант: источник читается последовательно, а __iter__() возвращает self. Это предотвращает рассинхронизацию состояния и явно отражает одноразовую природу потока.

Что кандидаты часто упускают

  1. Почему список не возвращает сам себя из iter()?

    Список — это итерируемый контейнер, а не итератор. Он может поддерживать несколько независимых обходов одновременно, поэтому iter(список) создаёт отдельный объект list_iterator, хранящий текущую позицию. Сам список при этом не изменяет состояние обхода.

  2. Может ли __iter__() итератора вернуть другой объект?

    Для объекта, претендующего на роль итератора, это нарушает ожидаемый контракт iter(x) is x. Формально возвращённый объект может оказаться пригодным для обхода, но исходный объект перестаёт вести себя как единый итератор: повторные вызовы iter() могут порождать независимые состояния и ломать предположения стандартного Python-кода.

  3. Вызывает ли цикл for iter() перед каждой итерацией?

    Нет. При запуске цикла Python получает итератор один раз, а затем последовательно вызывает у него __next__(), пока не получит StopIteration. Поэтому возвращение самого себя из __iter__() гарантирует, что все шаги данного цикла используют один и тот же объект и его единое состояние.