На этом шаге мы рассмотрим создание конечного итератора.
На этом этапе у вас уже должно сложиться довольно хорошее понимание того, как итератор работает в Python. Но пока что мы реализовывали только такие итераторы, которые продолжают выполнять итерации бесконечно.
Очевидно, бесконечное повторение не является главным вариантом использования итераторов в Python. На самом деле, когда вы обратитесь к самому началу нашего изложения, касающегося итераторов, то увидите, что в качестве мотивирующего примера использовался приведенный ниже фрагмент кода:
numbers = [1, 2, 3] for n in numbers: print(n)
Вы вправе ожидать, что этот код выведет числа 1, 2 и 3, а затем остановится. И вероятно, вы не ожидаете, что он захламит окно вашего терминала, без устали выводя "3", пока вы в дикой панике не начнете жать на CTRLl+C...
Пора узнать, как написать итератор, который в итоге прекращает генерировать новые значения вместо выполнения бесконечных итераций, потому что это именно то, что обычно делают объекты Python, когда мы используем их в цикле for ... in.
Сейчас мы напишем еще один класс итератора, который назовем ограниченным повторителем BoundedRepeater. Он будет похож на наш предыдущий пример с повторителем Repeater, но на этот раз мы хотим, чтобы он останавливался после предопределенного количества повторений.
Давайте задумаемся. Как это сделать? Как итератор сигнализирует о том, что он пуст и исчерпал элементы, выдаваемые во время выполнения итераций? Возможно, вы думали: "Хм, можно вернуть None из метода __next__(), и все".
И знаете, это неплохая идея, но проблема в следующем: что делать, если нам нужно, чтобы некий итератор был в состоянии возвращать None в качестве приемлемого значения?
Давайте посмотрим, что для решения этой проблемы делают другие итераторы Python. Создадим простой контейнер, список с несколькими элементами, а затем буду выполнять его итеративный обход до тех пор, пока он не исчерпает элементы, чтобы увидеть, что произойдет:
>>> my_list = [1, 2, 3] >>> iterator = iter(my_list) >>> next(iterator) 1 >>> next(iterator) 2 >>> next(iterator) 3
А теперь осторожно! Мы употребили все три имеющихся в списке элемента. Следите за тем, что произойдет, если еще раз вызвать метод next() итератора:
>>> next(iterator) Traceback (most recent call last): . . . . StopIteration
Ага! Чтобы подать сигнал о том, что мы исчерпали все имеющиеся в итераторе значения, он вызывает исключение StopIteration.
Все верно: итераторы используют исключения для структуризации потока управления. Чтобы подать сигнал о завершении итераций, итератор Python просто вызывает встроенное исключение StopIteration.
Если продолжить запрашивать значения из итератора, он продолжит вызывать исключения StopIteration, сигнализируя о том, что больше нет значений, доступных для итераций:
>>> next(iterator)
Traceback (most recent call last):
. . . .
StopIteration
Теперь мы знаем все, что нужно для написания нашего класса BoundedRepeater, который прекращает итерации после заданного количества повторений:
>>> class BoundedRepeater: def __init__(self, value, max_repeats): self.value = value self.max_repeats = max_repeats self.count = 0 def __iter__(self): return self def __next__(self): if self.count >= self.max_repeats: raise StopIteration self.count += 1 return self.value
И он дает нам требуемый результат. Итерации прекращаются после ряда повторений, определенных в параметре max_repeats:
>>> repeater = BoundedRepeater('Привет', 3) >>> for item in repeater: print(item) Привет Привет Привет
Если переписать этот последний пример цикла for ... in, устранив часть синтаксического сахара, то в итоге мы получим следующий ниже расширенный фрагмент кода:
repeater = BoundedRepeater('Привет', 3) iterator = iter(repeater) while True: try: item = next(iterator) except StopIteration: break print(item)
При каждом вызове функции next() в этом цикле мы выполняем проверку на исключение StopIteration и при необходимости выходим из цикла while.
Возможность написать трехстрочный цикл for ... in вместо восьмистрочного цикла while представляет собой вполне хорошее улучшение. И в результате программный код становится проще для восприятия и удобнее в сопровождении. И это еще одна причина, почему в Python итераторы являются таким мощным инструментом.
На следующем шаге мы рассмотрим совместимость с Python 2.x.