Итераторы своими руками в Python: протокол __iter__ и __next__
Разбираем, как for на самом деле обходит объекты, чем итератор отличается от итерируемого и почему одноразовый итератор ломается при повторном проходе.

Что происходит, когда вы пишете for x in obj
Цикл for не знает ни про списки, ни про файлы, ни про ваши классы. Он знает ровно два вызова: получить у объекта итератор через iter() и дёргать у итератора next(), пока тот не скажет «всё». Всё остальное — детали конкретного объекта. Разберём протокол на CPython 3.12, соберём собственный итератор и посмотрим, где самодельная реализация ломается на повторном проходе и на StopIteration внутри генератора.
Практический вопрос, ради которого стоит понять протокол: почему объект, который прекрасно работает в первом for, во втором молча выдаёт пустоту. Это не баг интерпретатора, это следствие того, что вы перепутали итерируемое с итератором.
Два разных объекта: iterable и iterator
В обиходе их сваливают в кучу, и отсюда половина ошибок. Разница жёсткая:
- Iterable (итерируемое) — объект, у которого есть метод
__iter__(). Он умеет по запросу выдать новый итератор. Список, словарь, строка — итерируемые. - Iterator (итератор) — объект, у которого есть
__next__(), и который по соглашению возвращает сам себя из__iter__(). Он хранит позицию обхода и одноразов: дошёл до конца — исчерпан навсегда.
Список не является итератором: у [1, 2, 3] нет __next__. Зато при каждом iter([1, 2, 3]) вы получаете свежий list_iterator со сброшенной позицией. Поэтому по списку можно пройти циклом сколько угодно раз, а по итератору — один.
Проверить это можно руками, без всякого for:
# Python 3.12
numbers = [10, 20, 30]
it = iter(numbers) # получили итератор из итерируемого
print(type(it)) # <class 'list_iterator'>
print(next(it)) # 10
print(next(it)) # 20
print(next(it)) # 30
print(next(it)) # StopIteration — исключение, а не None
Последняя строка бросит StopIteration. Именно это исключение цикл for ловит внутри себя и понимает как сигнал «обход закончен».
Собираем итератор руками
Напишем класс, который выдаёт числа от start до stop с шагом — упрощённый range, но как честный итератор. Важно сразу решить, кто хранит позицию.
Наивная и опасная реализация: iterable и iterator в одном классе
# Python 3.12 — так делать НЕ стоит, разберём почему
class CountUp:
def __init__(self, start, stop, step=1):
self.start = start
self.stop = stop
self.step = step
self.current = start # позиция живёт прямо в объекте
def __iter__(self):
return self # возвращаем самого себя
def __next__(self):
if self.current >= self.stop:
raise StopIteration
value = self.current
self.current += self.step
return value
Работает в первом цикле и предаёт во втором:
counter = CountUp(0, 3)
print(list(counter)) # [0, 1, 2]
print(list(counter)) # [] — позиция self.current осталась на 3
Объект совместил роли: он и итерируемый (есть __iter__), и итератор (есть __next__ и __iter__ возвращает self). Позиция одна на всех, сбросить её нечем. Так устроены генераторы и файловые объекты — они одноразовы осознанно. Но для «диапазона», который ждут многоразовым, это ловушка.
Правильное разделение ролей
Итерируемое хранит только параметры. Позицию заводит отдельный итератор, который создаётся заново на каждый __iter__.
# Python 3.12
class CountUp:
"""Итерируемое: помнит параметры, но не позицию."""
def __init__(self, start, stop, step=1):
self.start = start
self.stop = stop
self.step = step
def __iter__(self):
# каждый вызов — новый итератор с нуля
return _CountUpIterator(self.start, self.stop, self.step)
class _CountUpIterator:
"""Итератор: хранит позицию, одноразовый."""
def __init__(self, start, stop, step):
self.current = start
self.stop = stop
self.step = step
def __iter__(self):
return self
def __next__(self):
if self.current >= self.stop:
raise StopIteration
value = self.current
self.current += self.step
return value
Теперь оба прохода честные:
counter = CountUp(0, 3)
print(list(counter)) # [0, 1, 2]
print(list(counter)) # [0, 1, 2] — новый итератор, новая позиция
Правило, которое стоит повесить перед глазами: если объект возвращает из__iter__самого себя, он одноразовый. Многоразовость появляется только тогда, когда__iter__создаёт новый объект каждый раз.
Генератор делает то же самое короче
Всё, что мы написали, — это то, что генератор собирает за вас. Ключевое слово yield превращает функцию в фабрику итераторов: интерпретатор сам сохраняет позицию между вызовами next и сам бросает StopIteration в конце.
# Python 3.12
def count_up(start, stop, step=1):
current = start
while current < stop:
yield current
current += step
print(list(count_up(0, 3))) # [0, 1, 2]
Но заметьте разницу с классом CountUp: генераторная функция при каждом вызове возвращает новый генератор, а вот сам генератор одноразов. То есть count_up(0, 3) — фабрика, результат её вызова — итератор на один проход. Если сохранить генератор в переменную и пройти дважды, второй раз получите пусто — как в наивном классе выше.
Когда писать класс, а когда генератор
| Критерий | Класс с __iter__/__next__ | Генераторная функция |
|---|---|---|
| Простой линейный обход | избыточно | подходит |
| Нужен многоразовый обход одного объекта | да, __iter__ отдаёт новый итератор | нужна обёртка-класс поверх |
| Нужно состояние и методы помимо обхода | да | нет |
| Сложное ветвление, вложенные yield | громоздко | читаемо |
| Объём кода | больше | меньше |
Практический вывод: если вам нужен только обход и одного прохода достаточно — берите генератор. Если объект должен быть многоразовым итерируемым или у него есть жизнь помимо перебора — пишите класс, где __iter__ порождает генератор или отдельный итератор.
Класс, отдающий генератор из __iter__
Гибрид, который встречается чаще всего: логику обхода описываем через yield, а многоразовость обеспечивает класс.
# Python 3.12
class CountUp:
def __init__(self, start, stop, step=1):
self.start = start
self.stop = stop
self.step = step
def __iter__(self):
current = self.start
while current < self.stop:
yield current
current += self.step
Каждый вызов __iter__ запускает тело функции заново — получается свежий генератор. Проход по объекту многоразовый, а кода почти столько же, сколько в генераторной функции.
Типичные ошибки
- Вернуть
Noneвместоraise StopIteration. Циклforне проверяет возвращаемое значение наNone— он будет крутиться бесконечно, пока не поймаетStopIteration. Забыли бросить исключение — получили зависание. __iter__возвращаетselfтам, где нужен новый итератор. Объект становится одноразовым незаметно: тесты с одним проходом зелёные, в проде второйforпо тому же объекту даёт пустоту.returnвнутри генератора с ошибкой. В Python 3.7+ еслиStopIterationподнимается внутри тела генератора (например, вы вручную вызвалиnext()у исчерпанного вложенного итератора без обработки), она превращается вRuntimeErrorпо PEP 479. Не пробрасывайте чужуюStopIterationнаружу — оборачивайте вызовыnextвtry/exceptили используйтеfor.- Мутировать коллекцию во время обхода. Встроенные итераторы списков и словарей бросят
RuntimeError: dictionary changed size during iterationили тихо пропустят элементы. Собирайте изменения отдельно и применяйте после цикла.
Проверить, что перед вами
Стандартная библиотека даёт абстрактные базовые классы для проверки:
# Python 3.12
from collections.abc import Iterable, Iterator
numbers = [1, 2, 3]
it = iter(numbers)
print(isinstance(numbers, Iterable)) # True
print(isinstance(numbers, Iterator)) # False — список не итератор
print(isinstance(it, Iterator)) # True
print(isinstance(it, Iterable)) # True — у итератора есть __iter__
Обратите внимание: любой итератор одновременно является итерируемым, потому что обязан иметь __iter__. Обратное неверно — итерируемое не обязано быть итератором.
Что делать дальше
Если разобрались с протоколом, следующий шаг — посмотреть модуль itertools: count, cycle, islice, chain. Это готовые итераторы, написанные на C, и почти всё, что вы стали бы писать классом руками, там уже есть. Для ленивой обработки больших потоков данных смотрите на генераторные выражения и yield from для делегирования вложенным итераторам. Инвалидация состояния при исключениях внутри обхода — отдельная тема, здесь мы касались только базового протокола.
Транскрибатор встреч: Запись созвона или диктофона — в текст с разделением по спикерам, протоколом и списком задач.
Спросить за 25 ₽Источники: Python Language Reference — Iterator Types, PEP 479 — Change StopIteration handling inside generators
Частые вопросы
Чем итератор отличается от итерируемого простыми словами?
Почему мой класс работает в первом цикле for, а во втором выдаёт пусто?
Что будет, если забыть бросить StopIteration в __next__?
А если у меня Python 3.6 или раньше — что-то отличается?
Когда вообще писать класс, а не генераторную функцию?
Как проверить, является ли объект итератором в коде?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.