Посмотрим на свойства класса, который планируется использовать как итератор:

<aside> 👉 Если класс реализовывает два магических метода выше, то он говорят, что он реализовывает протокол итератора.

</aside>

Простой итератор

Попробуем создать класс с учётом всех этих свойств. Напишем класс, который будет бесконечно возвращать одно и то же значение:

class Repeater:
    def __init__(self, value):      # Параметр конструктора - само значение
        self.value = value

    def __iter__(self):             # Возвращаем сам объект в качестве итератора
        return self

    def __next__(self):             # При вызове в цикле возвращаем значение
        return self.value

for element in Repeater('Hello!'):  # Бесконечный вывод "Hello!"
    print(element)

Если это развернуть в цикл while, то получим следующее:

class Repeater:
    def __init__(self, value):      # Параметр конструктора - само значение
        self.value = value

    def __iter__(self):             # Возвращаем сам объект в качестве итератора
        return self

    def __next__(self):             # При вызове в цикле возвращаем значение
        return self.value

iterator = iter(Repeater('Hello!'))
while True:
    try:
        element = next(iterator)
        print(element)
    except StopIteration:
        break

Здесь можно увидеть, почему цикл for никогда не завершается: метод __next()__ никогда не возвращает исключение StopIteration, поэтому инструкция break никогда не выполнится.

Более сложный пример

Попробуем написать класс-итератор, который будет генерировать степени двойки до определённого момента:

class PowTwo:
    def __init__(self, max_pow: int) -> None:
        # Параметр - максимальный показатель степени
        self.__max_pow = max_pow

        # Дополнительное поле для хранения
        # текущего показателя степени
        self.__cur_pow = 0

    def __iter__(self) -> 'PowTwo':
        # Возвращаем состояние к исходному на случай,
        # если один объект нужно будет использовать
        # несколько раз
        self.__cur_pow = 0
        return self

    def __next__(self) -> int:
        # Если текущая степень больше максимальной,
        # то прерываем итерирование
        if self.__max_pow < self.__cur_pow:
            raise StopIteration

        # В противном случае возвращаем степень двойки
        # и увеличиваем показатель степени на 1
        result = 2 ** self.__cur_pow
        self.__cur_pow += 1
        return result

# Перебираем степени двойки от 2^0 до 2^10 и выводим их
pow_to_10 = PowTwo(10)

# Делаем это один раз...
for element in pow_to_10:
    print(element)

# И второй, чтобы показать, что можно использовать
# один и тот же объект в нескольких итерациях
for element in pow_to_10:
    print(element)

Зачем нам это нужно?

Вполне логичный вопрос, который было бы справедливо задать. Основное назначение итераторов — генерация последовательностей без хранения всех её элементов.

Рассмотрим пример. Если у нас будет файл с таблицей, которую нужно обработать построчно, то можно пойти двумя путями:

Первый способ, конечно, проще, но если данных будет так много, что они не поместятся в оперативную память, то вы просто не сможете их обработать, получив исключение MemoryError. Второй способ в этом плане на порядки оптимальнее.