Протокол итератора Python: iter, next и StopIteration
Цикл for умеет обходить списки, строки, файлы и пользовательские объекты благодаря единому протоколу. Итерируемый объект предоставляет итератор через iter(obj), а итератор выдаёт…
Цикл for умеет обходить списки, строки, файлы и пользовательские объекты благодаря единому протоколу. Итерируемый объект предоставляет итератор через iter(obj), а итератор выдаёт очередные элементы через next(iterator). Когда значений больше нет, метод __next__ обязан возбудить StopIteration; цикл перехватывает этот сигнал и завершается без ошибки на экране.
Итерируемый объект и итератор
Эти роли не всегда принадлежат одному объекту. Список можно обходить снова и снова: каждый вызов iter(numbers) создаёт новый независимый итератор. Сам итератор хранит текущую позицию, возвращает себя из __iter__ и обычно одноразовый. Поэтому iter(iterator) is iterator истинно, но два итератора одного списка могут находиться на разных позициях.
Ручной вызов next полезен при разборе протокола и точечном чтении потока. В прикладном коде for, распаковка и генераторы часто безопаснее, поскольку корректно обрабатывают окончание последовательности.
Рабочий пример: собственный обратный отсчёт
class Countdown:
def __init__(self, start: int) -> None:
if isinstance(start, bool) or not isinstance(start, int):
raise TypeError('start должен быть целым числом')
if start < 0:
raise ValueError('start не может быть отрицательным')
self.current = start
def __iter__(self):
return self
def __next__(self):
if self.current == 0:
raise StopIteration
value = self.current
self.current -= 1
return value
counter = Countdown(3)
print(next(counter))
print(list(counter))
print(list(counter))Ожидаемый результат:
3
[2, 1]
[]Первый next сдвигает состояние. Затем list забирает оставшиеся значения, а третья попытка видит уже исчерпанный итератор. Конструктор не допускает отрицательный, дробный и логический старт, поэтому состояние гарантированно дойдёт до нуля. Такой класс совмещает контейнер и курсор; для повторяемого обхода удобнее сделать отдельный объект-коллекцию, чей __iter__ каждый раз создаёт новый итератор.
Когда итератор исчерпан
Если вызвать next(counter) после исчерпания напрямую, наружу выйдет StopIteration. Это не поломка протокола, а его штатный сигнал, который ручной код не перехватил. Для необязательного чтения можно использовать второй аргумент: next(counter, 'конец') вернёт маркер вместо исключения.
Другая характерная проблема — TypeError: iter() returned non-iterator. Проверьте, что __iter__ возвращает объект с __next__, а не список значений. При пропусках или повторениях временно печатайте self.current до изменения и перед return: состояние следует обновлять ровно один раз на успешно выданный элемент. StopIteration нужно возбуждать до возврата несуществующего значения.
Тренировка: диапазон с собственным шагом
Реализуйте итератор StepRange(start, stop, step) для положительного шага без использования встроенного range. Он должен выдавать start, затем прибавлять step и останавливаться перед stop. Для StepRange(2, 9, 3) ожидается [2, 5, 8].
Самостоятельно выполните два next, затем передайте тот же объект в list и предскажите остаток. Отдельно создайте второй экземпляр и подтвердите, что его позиция независима. Добавьте проверку конструктора, запрещающую нулевой и отрицательный шаг понятным ValueError.
Правила протокола обхода
iter получает итератор, next запрашивает одно значение, а StopIteration обозначает штатное окончание. Цикл for выполняет эти действия автоматически. Итерируемый контейнер может создавать много курсоров, тогда как конкретный итератор несёт изменяемое состояние и после исчерпания не начинает работу заново сам по себе.
Итераторы после первого прохода
Почему второй цикл по итератору ничего не выводит?
Первый цикл уже довёл его до StopIteration. Создайте новый итератор из исходного итерируемого объекта или спроектируйте __iter__ так, чтобы он возвращал новый курсор.
Можно ли вернуть None вместо StopIteration?
Нет, если None может быть обычным элементом. Протокол различает значение и окончание именно исключением StopIteration; возврат None создаст ещё один элемент последовательности.
Зачем у next есть значение по умолчанию?
next(iterator, default) удобно, когда отсутствие очередного элемента ожидаемо. Оно заменяет только StopIteration заданным значением и не скрывает другие исключения внутри __next__.