Создание собственного итератора
В прошлом уроке мы разобрались, как работает протокол итератора изнутри. Теперь мы напишем свой итератор с нуля. Вы поймёте, что скрывается за методами __iter__ и __next__, увидите типичную ловушку, в которую попадают новички, и узнаете, как её обойти правильно.
🔧 Минимальная реализация: что нужно для собственного итератора
Чтобы Python считал ваш объект итератором, ему нужно ровно два метода:
🔁
__iter__(self)
Должен вернуть объект итератора. В простейшем случае — возвращает self (самого себя). Именно этот метод вызывает Python, когда пишешь for x in obj или iter(obj).
➡️
__next__(self)
Должен вернуть следующий элемент. Когда элементов больше нет — обязан поднять raise StopIteration. Именно этот метод вызывает Python при каждом шаге цикла for.
Напишем первый собственный итератор — простой счётчик от 0 до N:
class CounterIterator:
"""Счётчик от 0 до N (не включая N). Это — итератор."""
def __init__(self, limit):
self.limit = limit # до какого числа считать
self.current = 0 # текущая позиция, начинаем с 0
def __iter__(self):
# Итератор обязан иметь __iter__, возвращающий self.
# Это позволяет использовать его там, где ожидается Iterable.
return self
def __next__(self):
if self.current >= self.limit:
# Элементов больше нет — поднимаем сигнал об окончании
raise StopIteration
value = self.current # запоминаем текущее значение
self.current += 1 # сдвигаем позицию вперёд
return value # возвращаем запомненное
# Используем как обычный range:
counter = CounterIterator(5)
for number in counter:
print(number)
# Вывод:
# 0
# 1
# 2
# 3
# 4
💡 Разберём по шагам: когда Python встречает for number in counter, он вызывает counter.__iter__() — получает self (сам объект). Затем на каждом шаге вызывает counter.__next__(). Как только self.current достигает self.limit — метод поднимает StopIteration, и цикл завершается.
Можно также использовать функции iter() и next() вручную — всё работает так же:
counter = CounterIterator(3)
it = iter(counter) # вызывает counter.__iter__() → возвращает self
print(next(it)) # 0
print(next(it)) # 1
print(next(it)) # 2
# print(next(it)) # StopIteration — счётчик исчерпан
🚫 Антипаттерн: один класс — и Iterable, и Iterator одновременно
Наш CounterIterator — это уже и Iterable (есть __iter__), и Iterator (есть __next__) одновременно. На первый взгляд кажется удобным: один класс, всё в одном месте. Но у такого подхода есть серьёзная проблема.
⚠️ Проблема: второй for-цикл будет пустым!
Итератор хранит состояние — текущую позицию. Как только он прошёл до конца, он «исчерпан». Если вы попробуете обойти его вторым циклом — он сразу получит StopIteration и выведет ноль элементов.
# АНТИПАТТЕРН — демонстрация проблемы
class BadRange:
"""Плохой пример: один класс — и Iterable, и Iterator."""
def __init__(self, limit):
self.limit = limit
self.current = 0 # состояние — в самом объекте
def __iter__(self):
return self # возвращаем СЕБЯ — вот в чём проблема
def __next__(self):
if self.current >= self.limit:
raise StopIteration
value = self.current
self.current += 1
return value
bad = BadRange(3)
# Первый цикл — работает отлично
print("Первый цикл:")
for x in bad:
print(x)
# Вывод: 0, 1, 2
# Второй цикл — ПУСТОЙ! Итератор уже исчерпан.
print("Второй цикл:")
for x in bad:
print(x)
# Вывод: (ничего)
# Почему? Потому что bad.current уже равен 3.
# Когда for вызывает iter(bad), получает ТOGO ЖЕ САМОГО bad.
# А bad.__next__() сразу бросает StopIteration — позиция не сброшена.
print(f"bad.current = {bad.current}") # 3 — не сбросился в 0!
🔴 Это скрытый баг: код не падает с ошибкой — он просто тихо пропускает данные. Вы ожидаете вывода, а получаете пустоту. Такие ошибки очень трудно найти в большом проекте.
Та же проблема возникнет, если передать такой объект в несколько мест одновременно — например, в две функции, которые обе перебирают элементы: одна из них получит все данные, вторая — пустой результат.
✅ Правильный подход: разделить Iterable и Iterator
Решение простое и элегантное: один класс хранит данные (Iterable), а другой — отслеживает позицию (Iterator). Каждый раз, когда нужен новый обход, создаётся свежий итератор.
📚 Аналогия: книга и закладка
Книга (Iterable) — хранит все страницы. Она не помнит, где вы остановились. Её можно читать заново сколько угодно раз.
Закладка (Iterator) — помнит текущую страницу. Каждый читатель берёт свою закладку. Можно иметь несколько закладок в одной книге одновременно.
class NumberRangeIterator:
"""
ИТЕРАТОР: хранит текущую позицию, умеет двигаться вперёд.
Создаётся заново каждый раз, когда нужен новый обход.
"""
def __init__(self, start, stop):
self.current = start # начальная позиция
self.stop = stop # до какого числа (не включая)
def __iter__(self):
# Итератор тоже должен быть итерируемым — возвращает себя.
return self
def __next__(self):
if self.current >= self.stop:
raise StopIteration
value = self.current
self.current += 1
return value
class NumberRange:
"""
ITERABLE: хранит параметры диапазона (start, stop).
При каждом for — создаёт НОВЫЙ итератор.
Можно обходить сколько угодно раз!
"""
def __init__(self, start, stop):
self.start = start
self.stop = stop
def __iter__(self):
# Ключевой момент: создаём НОВЫЙ итератор каждый раз!
return NumberRangeIterator(self.start, self.stop)
# Создаём Iterable-объект один раз
numbers = NumberRange(1, 4)
# Первый цикл — работает
print("Первый цикл:")
for x in numbers:
print(x)
# Вывод: 1, 2, 3
# Второй цикл — тоже работает с начала!
print("Второй цикл:")
for x in numbers:
print(x)
# Вывод: 1, 2, 3 — снова с начала, как будто первого цикла не было
# Параллельные итераторы — каждый независим:
it1 = iter(numbers) # новый итератор
it2 = iter(numbers) # ещё один новый итератор
print(next(it1)) # 1
print(next(it1)) # 2
print(next(it2)) # 1 — it2 начал с начала, не зависит от it1!
print(next(it1)) # 3
print(next(it2)) # 2
🎯 Ключевое отличие: в NumberRange.__iter__ мы пишем return NumberRangeIterator(...) — создаём новый объект. В плохом варианте мы писали return self — возвращали тот же объект. Это одно маленькое отличие — а разница в поведении огромная.
Именно так работают все встроенные коллекции Python: list, range, str — это Iterable-объекты. Когда вы пишете for x in my_list, Python создаёт новый list_iterator каждый раз.
🗂️ Практический пример: своя коллекция слов
Теперь применим паттерн к реальной задаче. Создадим класс Words — коллекцию слов, которую можно перебирать в цикле. Она будет правильным Iterable с отдельным Iterator.
class WordsIterator:
"""Итератор для коллекции Words. Хранит текущую позицию."""
def __init__(self, words_list):
self._words = words_list # ссылка на список слов
self._index = 0 # текущий индекс
def __iter__(self):
return self
def __next__(self):
if self._index >= len(self._words):
raise StopIteration
word = self._words[self._index]
self._index += 1
return word
class Words:
"""
Коллекция слов — правильный Iterable.
Поддерживает добавление слов и многократный обход.
"""
def __init__(self):
self._data = [] # внутренний список слов
def add(self, word):
"""Добавить слово в коллекцию."""
self._data.append(word)
def __len__(self):
"""Количество слов (бонус: поддержка len())."""
return len(self._data)
def __iter__(self):
"""Создаём новый итератор при каждом обходе."""
return WordsIterator(self._data)
# Создаём коллекцию и наполняем её
vocabulary = Words()
vocabulary.add("питон")
vocabulary.add("класс")
vocabulary.add("объект")
vocabulary.add("метод")
vocabulary.add("итератор")
print(f"Слов в коллекции: {len(vocabulary)}") # 5
# Первый обход
print("\nПервый обход:")
for word in vocabulary:
print(f" - {word}")
# Второй обход — с начала!
print("\nВторой обход:")
for word in vocabulary:
print(f" - {word}")
# Ищем слово (используем итерацию)
print("\nПоиск слов с буквой 'о':")
for word in vocabulary:
if "о" in word:
print(f" Найдено: {word}")
# Параллельные обходы независимы
print("\nПараллельные итераторы:")
it_a = iter(vocabulary)
it_b = iter(vocabulary)
print(next(it_a)) # питон
print(next(it_b)) # питон — it_b начал независимо
print(next(it_a)) # класс
print(next(it_a)) # объект
print(next(it_b)) # класс — it_b отстаёт, но независим
🌟 Обратите внимание: класс Words совсем не знает о том, как происходит итерация. Он просто хранит данные и делегирует итерацию классу WordsIterator. Это хорошее разделение ответственностей.
⚖️ Когда один класс — нормально, а когда нет
Теперь вы знаете оба подхода. Давайте чётко обозначим, когда какой применять:
| Ситуация | Один класс | Два класса |
|---|---|---|
| Одноразовый поток данных (файл, сетевой поток) | ✅ Подходит | Избыточно |
| Коллекция, которую обходят несколько раз | 🚫 Второй for пустой! | ✅ Правильно |
| Два for-цикла по одному объекту одновременно | 🚫 Один получит пустоту | ✅ Независимы |
| Передача объекта в несколько функций | 🚫 Опасно | ✅ Безопасно |
📌 Практическое правило: если вы делаете итератор для одноразового прохода (генератор данных, чтение файла) — один класс нормален. Если вы делаете коллекцию — всегда разделяйте на два класса. Сомневаетесь? Разделяйте — это никогда не сломает код.
🗺️ Полная картина: как Iterable и Iterator работают вместе
Посмотрим на полный процесс с нашим NumberRange и NumberRangeIterator:
# Схема взаимодействия при for x in NumberRange(1, 4):
# 1. Python вызывает iter(numbers)
# → numbers.__iter__()
# → создаётся NumberRangeIterator(1, 4), current=1
# 2. Python вызывает next(iterator)
# → iterator.__next__()
# → current=1, возвращает 1, current становится 2
# 3. Тело цикла выполняется: print(1)
# 4. Python вызывает next(iterator) снова
# → current=2, возвращает 2, current становится 3
# 5. Тело цикла: print(2)
# 6. Python вызывает next(iterator) снова
# → current=3, возвращает 3, current становится 4
# 7. Тело цикла: print(3)
# 8. Python вызывает next(iterator) снова
# → current=4 >= stop=4 → raise StopIteration
# 9. Python ловит StopIteration → цикл завершён
# Если написать второй for — шаг 1 создаст НОВЫЙ итератор
# с current=1, и всё повторится сначала!
numbers = NumberRange(1, 4)
for x in numbers: # итератор №1, current: 1→4
print(x) # 1, 2, 3
for x in numbers: # итератор №2, свежий, current: 1→4
print(x) # 1, 2, 3 — всё работает!
✅ Итоги урока
- Минимальный итератор требует двух методов:
__iter__(возвращаетself) и__next__(возвращает следующий элемент или поднимаетStopIteration). - Антипаттерн: если один класс — и Iterable, и Iterator (возвращает
selfиз__iter__), второй циклforбудет пустым — итератор уже исчерпан. - Правильный паттерн: два отдельных класса — Iterable хранит данные, Iterator хранит позицию. Метод
__iter__Iterable-класса создаёт новый экземпляр Iterator. - Это позволяет обходить коллекцию сколько угодно раз и иметь несколько независимых итераторов одновременно.
- Именно так устроены все встроенные коллекции Python:
list.__iter__()возвращает новыйlist_iterator, а не самого себя.
👉 В следующем уроке мы познакомимся с генераторами — особым видом функций, которые делают то же самое, что итераторы, но с несравнимо меньшим количеством кода. Слово yield полностью изменит то, как вы думаете о последовательностях в Python.