Перейти к содержанию

252 уроков, 17 библиотек и челлендж «Что выведет код?» — бесплатно, код прямо в браузере

Начать обучение
Урок 15 из 20 Средний 35 мин 140 XP

Итераторы: __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 — выход. Проверим протокол руками на обычном списке:

iter() достаёт итератор, next() тянет элементы
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
итератор исчерпан
for под капотом: тот же протокол вручную
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__

__iter__ с yield — итератор без класса-обёртки
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))
Проверь себя
0 / 6

1. Что цикл for делает на старте итерации по obj?

2. Как итератор сообщает, что элементы закончились?

3. В Countdown метод __iter__ возвращает self. Что получит второй list(count) по одному объекту?

4. Что минимально нужно, чтобы экземпляры класса работали в for?

5. Что делает запись yield from self.books внутри метода __iter__?

6. Чем итерируемый объект отличается от итератора?

Карточки терминов
Запомнено: 0 / 7
Практика

Напишите класс Fib — итератор чисел Фибоначчи: конструктор принимает limit, а обход выдаёт числа Фибоначчи, не превышающие limit, начиная с 1, 1. Реализуйте __iter__ генератором с yield и пройдите Fib(30) циклом for, печатая каждое число.

practice.py
Вопросы и ответы по уроку

Что такое итератор в 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-канал или свой блог — так о проекте узнают новые читатели.

TelegramVK

Похожие уроки по темам

Подобраны автоматически по пересечению тем и ключевых слов.