'Привет'
>>> next(iterator)
'Привет'
>>> next(iterator)
'Привет'
...
Этот фрагмент кода дает тот же самый результат — бесконечный поток
приветствий. Всякий раз, когда вы вызываете
next()
, итератор снова вы-
дает то же самое приветствие.
Между прочим, здесь я воспользовался возможностью замены вызо-
вов
__iter__
и
__next__
на вызовы встроенных в Python функций
iter()
и
next()
.
На внутреннем уровне эти встроенные функции вызывают те же самые
дандер-методы, но они делают программный код немного симпатичнее
и более удобочитаемым, предоставляя протоколу итератора чистый «фа-
сад».
Python предлагает эти фасады также и для другой функциональности. На-
пример,
len(x)
является краткой формой для вызова
x.__len__
. Точно так
же вызов функции
iter(x)
вызывает метод
x.__iter__
, а вызов функции
next(x)
вызывает метод
x.__next__
.
В целом неплохая идея использовать встроенные фасадные функции,
вместо того чтобы непосредственно обращаться к дандер-методам, реа-
218 Глава 6 • Циклы и итерации
лизующим протокол итератора. Это намного упрощает восприятие ис-
ходного кода.
Более простой класс-итератор
До этого момента наш пример итератора состоял из двух отдельных клас-
сов,
Repeater
и
RepeaterIterator
. Они соответствовали непосредственно
двум фазам, используемым в протоколе итератора Python: сначала подго-
товке и получению объекта-итератора через вызов функции
iter()
, а затем
неоднократной доставке из него значений через вызов функции
next()
.
Во многих случаях обе эти функциональные обязанности можно взва-
лить на один-единственный класс. Это позволит сократить объем про-
граммного кода, необходимого для написания итератора, основанного
на классах.
Я решил этого не делать с первым примером в данном разделе, потому что
это внесло бы путаницу в чистоту ментальной модели в основе протокола
итератора. Но теперь, когда вы увидели, как писать итератор на основе
классов более долгим и более сложным способом, давайте потратим еще
минуту, чтобы упростить то, что у нас есть на данный момент.
Помните, почему нам вновь потребовался класс
RepeaterIterator
? Он
был нужен, чтобы принять метод
__next__
для доставки новых значений
из итератора. Но место определения метода
__next__
вовсе не имеет ни-
какого значения. В протоколе итератора имеет значение только то, что
метод
__iter__
возвращает любой объект с определенным на нем методом
__next__
.
Поэтому идея такая:
RepeaterIterator
без конца возвращает одинаковое
значение, и он не должен отслеживать никакое внутреннее состояние.
Что, если вместо этого добавить метод
__next__
непосредственно в класс
Repeater
?
Тем самым мы смогли бы целиком избавиться от
RepeaterIterator
и реа-
лизовать итерируемый объект при помощи одного-единственного класса
Python. Давайте попробуем! Наш пример с новым и упрощенным итера-
тором выглядит так:
6 .4 . Красивые итераторы 219
class Repeater:
def __init__(self, value):
self.value = value
def __iter__(self):
return self
def __next__(self):
return self.value
Мы только что перешли от двух отдельных классов и десяти строк кода
всего к одному классу и семи строкам кода. Наша упрощенная реализация
по-прежнему без проблем поддерживает протокол итератора:
>>> repeater = Repeater('Привет')
>>> for item in repeater:
... print(item)
Привет
Привет
Привет
...
В подобной оптимизации итератора на основе класса часто есть смысл. По
сути, большинство пособий Python по итераторам начинается именно так.
Но я всегда чувствовал, что объяснять итераторы одним-единственным
классом с самого начала — значит скрывать основные принципы прото-
кола итератора и по этой причине еще больше затруднять его понимание.
Кто же захочет без конца выполнять итерации
На этом этапе у вас уже должно сложиться довольно хорошее понимание
того, как итератор работает в Python. Но пока что мы реализовывали
только такие итераторы, которые продолжают выполнять итерации бес-
конечно.
Очевидно, бесконечное повторение не является главным вариантом ис-
пользования итераторов в Python. На самом деле, когда вы обратитесь
к самому началу этого раздела, то увидите, что в качестве мотивирующего
примера я использовал приведенный ниже фрагмент кода:
220 Глава 6 • Циклы и итерации
numbers = [1, 2, 3]
for n in numbers:
print(n)
Вы вправе ожидать, что этот код выведет числа
1
,
2
и
3
, а затем остано-
вится. И вероятно, вы не ожидаете, что он захламит окно вашего терми-
нала, без устали выводя «
3
», пока вы в дикой панике не начнете жать
на
Ctrl+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
6 .4 . Красивые итераторы 221
>>> next(iterator)
3
А теперь осторожно! Мы употребили все три имеющихся в списке элемента.
Следите за тем, что произойдет, если еще раз вызвать метод
next
итератора:
>>> next(iterator)
Do'stlaringiz bilan baham: |