Итераторы: __iter__ и __next__
Каждый for в Python под капотом вызывает iter() и next(), а о конце данных сообщает исключение StopIteration. Научим свои классы работать в цикле: сначала вручную через __iter__ и __next__, потом коротким путём — генератором с yield.
Редакция Питоники
Каждый раз, когда вы пишете for book in books, Python делает две невидимые вещи: запрашивает у коллекции итератор и просит его выдавать элементы по одному. Этот договор называется протоколом итерации, и в нём всего два метода и одно исключение. В этом уроке вскроем механизм: соберём итератор вручную через __iter__ и __next__, а потом заменим его однострочным генератором с yield.
Протокол итерации: iter() и next()
Итерируемый объект (iterable) умеет отдавать итератор: вызов iter(obj) на самом деле вызывает obj.__iter__(). Итератор выдаёт элементы по одному: вызов next(it) превращается в it.__next__(), а когда элементы кончились — возбуждается StopIteration. Цикл for работает ровно так: iter() на старте, next() на каждом шаге, ловушка StopIteration — выход. Проверим протокол руками на обычном списке:
digits = [10, 20, 30]
it = iter(digits)
print(next(it))
print(next(it))
print(next(it))
try:
next(it)
except StopIteration:
print("итератор исчерпан")
10 20 30 итератор исчерпан
fruits = ["яблоко", "груша", "слива"]
it = iter(fruits)
while True:
try:
fruit = next(it)
except StopIteration:
break
print(fruit)
яблоко груша слива
| Элемент протокола | Что это | Кто вызывает |
|---|---|---|
| iter(obj) | Запрашивает итератор: превращается в obj.__iter__() | for на старте, list(), распаковка |
| next(it) | Берёт следующий элемент: превращается в it.__next__() | for на каждом шаге |
| StopIteration | Сигнал «элементов больше нет» | for ловит его и выходит из цикла |
| yield в __iter__ | Превращает метод в генератор — итератор готов без лишнего класса | вы, вместо ручного __next__ |
Свой класс-коллекция: __iter__ и __next__
class Countdown:
def __init__(self, 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
for n in Countdown(3):
print(n)
count = Countdown(5)
print(list(count))
print(list(count))
3 2 1 [5, 4, 3, 2, 1] []
Генератор-метод: yield вместо __next__
class Bookshelf:
def __init__(self):
self.books = []
def add(self, title):
self.books.append(title)
def __iter__(self):
yield from self.books
shelf = Bookshelf()
shelf.add("Отцы и дети")
shelf.add("Обломов")
shelf.add("Воскресение")
for title in shelf:
print(title)
print(list(shelf))
print(list(shelf))
Отцы и дети Обломов Воскресение ['Отцы и дети', 'Обломов', 'Воскресение'] ['Отцы и дети', 'Обломов', 'Воскресение']
Сначала предскажи ответ в голове — это главный навык программиста.
class Step:
def __init__(self):
self.n = 0
def __iter__(self):
return self
def __next__(self):
self.n += 2
if self.n > 6:
raise StopIteration
return self.n
print(list(Step()))
data = [1, 2, 3]
it = iter(data)
print(next(it))
print(next(it))
print(list(it))
def gen():
yield "a"
yield "b"
g = gen()
print(next(g))
print(list(gen()))
print(next(g))
1. Что цикл for делает на старте итерации по obj?
2. Как итератор сообщает, что элементы закончились?
3. В Countdown метод __iter__ возвращает self. Что получит второй list(count) по одному объекту?
4. Что минимально нужно, чтобы экземпляры класса работали в for?
5. Что делает запись yield from self.books внутри метода __iter__?
6. Чем итерируемый объект отличается от итератора?
Напишите класс Fib — итератор чисел Фибоначчи: конструктор принимает limit, а обход выдаёт числа Фибоначчи, не превышающие limit, начиная с 1, 1. Реализуйте __iter__ генератором с yield и пройдите Fib(30) циклом for, печатая каждое число.
Что такое итератор в Python простыми словами?
Это объект-курсор с методом __next__: каждый вызов отдаёт следующий элемент, а когда данные кончились — возбуждает StopIteration. Получают итератор из коллекции функцией iter(). Список — это данные, итератор — указатель на текущее место в них.
Урок 15: протокол итерацииКак работает цикл for под капотом?
for вызывает iter(коллекция) на старте, затем next(итератор) на каждом шаге и ловит StopIteration как сигнал завершения. Поэтому в for работает всё, у чего есть __iter__, — списки, строки, словари, файлы и ваши классы, а индексы не обязательны.
Почему итератор нельзя проходить дважды?
Итератор хранит позицию и выгорает: после StopIteration он больше не выдаст значений — второй list(it) вернёт пустой список. Чтобы обходить коллекцию многократно, сделайте __iter__ генератором с yield: он будет создавать свежий итератор на каждый вызов, как Bookshelf из урока.
Чем генератор отличается от итератора?
Генератор — это итератор, созданный функцией с yield: Python сам реализует для него __next__ и StopIteration. Писать класс с __iter__ и __next__ вручную стоит, когда логика выдачи сложная и stateful; во всех остальных случаях генератор-метод короче и надёжнее.
Понравился урок? Сошлитесь на него
Скопируйте готовую ссылку в формате HTML, Markdown или чистый адрес и вставьте в статью на Habr, VC, Telegram-канал или свой блог — так о проекте узнают новые читатели.
Что читать дальше
ООП · Урок 14
Контекстные менеджеры: __enter__ и __exit__
Блок with гарантирует очистку даже при ошибке — секрет в двух магических методах, __enter__ и __exit__. Превратим свои классы в контекстные менеджеры: журнал сессии, гашение исключений и транзакция с откатом.
ООП · Урок 16
@dataclass: классы без бойлерплейта
Декоратор @dataclass пишет за вас __init__, __repr__ и __eq__ по описанным полям класса. Разберём значения по умолчанию, field(default_factory) для списков, frozen=True для неизменяемых объектов и __post_init__ для производных полей.
Похожие уроки по темам
Подобраны автоматически по пересечению тем и ключевых слов.
asyncio · Урок 15
Асинхронные итераторы: async for
async for — это цикл, который умеет ждать: следующий элемент может прийти через секунду, и пока его нет, событийный цикл занимается другими делами.
async for python асинхронный итераторасинхронный итератор python
json · Урок 11
Фильтрация данных: найти нужное в JSON
Включение с условием для отбора, next с default для первого подходящего, any и all для вопросов ко всей коллекции — работаем с выгрузкой заказов.
python next первый элемент по условиюpython найти объект в json
ООП · Урок 2
__init__ и self: как рождается объект
__init__ задаёт объекту стартовое состояние, а self — это сам объект внутри его методов. Разбираем механику вызова hero.heal(30) до последней детали.
как работает self