Посмотрим на свойства класса, который планируется использовать как итератор:
__next()__, который:
StopIteration, которое показывает, что итерирование закончено;__iter()__, который в идеале возвращает сам объект (если есть метод __init()__, то этого вполне достаточно).<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. Второй способ в этом плане на порядки оптимальнее.