Шаг 148.
Python: тонкости программирования.
Циклы и итерации. Красивые итераторы. Как закончить выполнение итератора

    На этом шаге мы рассмотрим создание конечного итератора.

    На этом этапе у вас уже должно сложиться довольно хорошее понимание того, как итератор работает в 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
Итераторы Python обычно не могут быть "обнулены" - как только они исчерпаны, им полагается вызывать исключение StopIteration при каждом вызове их функции next(). Чтобы возобновить итерации, вам нужно запросить свежий объект-итератор при помощи функции iter().

    Теперь мы знаем все, что нужно для написания нашего класса 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.




Предыдущий шаг Содержание Следующий шаг