Итераторы и итерируемые объекты в Python. Методы __next__ и __iter__
В английской документации по Python фигурируют два похожих слова – iterable и iterator. Обозначают они разное, хотя и имеющее между собой связь.
На русский язык iterable обычно переводят как итерируемый объект, а iterator – как итератор, или объект-итератор. С объектами обоих разновидностей мы уже сталкивались в курсе "Python. Введение в программирование", однако не делали на этом акцента.
Iterable и iterator – это не какие-то конкретные классы-типы, наподобие int
или list
. Это обобщения. Существует ряд встроенных классов, чьи объекты обладают возможностями iterable. Ряд других классов порождают объекты, обладающие свойствами итераторов.
Кроме того, мы можем сами определять классы, от которых создаются экземпляры-итераторы или итерируемые объекты.
Примером итерируемого объекта является список. Примером итератора – файловый объект. Список включает в себя все свои элементы, а файловый объект по-очереди "вынимает" из себя элементы и "забывает" то, что уже вынул. Также не ведает, что в нем содержится еще, так как это "еще" может вычисляться при каждом обращении или поступать извне. Например, файловый объект не знает сколько еще текста в связанном с ним файле.
Из такого описания должно быть понятно, почему один и тот же список мы можем перебирать сколько угодно раз, а с файловым объектом это можно сделать только единожды.
Зачем нужны объекты, элементы которых можно получить только один раз? Представьте, что текстовый файл большой. Если сразу загрузить его содержимое в память, то последней может не хватить. Также бывает удобно генерировать значения на лету, по требованию, если они нужны в программе только один раз. В противовес тому, как если бы они были получены все сразу и сохранены в списке.
У всех итераторов, но не итерируемых объектов, есть метод __next__. Именно его код обеспечивает выдачу очередного элемента. Каков этот код, зависит от конкретного класса. У файлового объекта это по всей видимости код, читающий очередную строку из связанного файла.
>>> f = open('text.txt') >>> f.__next__() 'one two\n' >>> f.__next__() 'three \n' >>> f.__next__() 'four five\n' >>> f.__next__() Traceback (most recent call last): File "<stdin>", line 1, in <module> StopIteration
Когда итератор выдал все свои значения, то очередной вызов __next__()
должен возбуждать исключение StopIteration
. Почему именно такое исключение? Потому что на него "реагирует" цикл for
. Для for
это сигнал останова.
Судя по наличию подчеркиваний у __next__
, он относится к методам перегрузки операторов. Он перегружает встроенную функцию next()
. То есть когда объект передается в эту функцию, то происходит вызов метода __next__()
этого объекта-итератора.
>>> f = open('text.txt') >>> next(f) 'one two\n' >>> next(f) 'three \n' >>> next(f) 'four five\n' >>> next(f) Traceback (most recent call last): File "<stdin>", line 1, in <module> StopIteration
Если объект итератором не является, то есть у него нет метода __next__
, то вызов функции next()
приведет к ошибке:
>>> a = [1, 2] >>> next(a) Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: 'list' object is not an iterator
Внутренний механизм работы цикла for
так устроен, что на каждой итерации он вызывает функцию next()
и передает ей в качестве аргумента объект, указанный после in
в заголовке. Как только next()
возвращает StopIteration
, цикл for
ловит это исключение и завершает свою работу.
Напишем собственный класс с методом __next__
:
>>> class A: ... def __init__(self, qty): ... self.qty = qty ... def __next__(self): ... if self.qty > 0: ... self.qty -= 1 ... return '+' ... else: ... raise StopIteration ... >>> a = A(3) >>> next(a) '+' >>> next(a) '+' >>> next(a) '+' >>> next(a) Traceback (most recent call last): File "<stdin>", line 1, in <module> File "<stdin>", line 9, in __next__ StopIteration
Вызов next()
работает, но если мы попробуем передать объект циклу for
, получим ошибку:
>>> b = A(5) >>> for i in b: ... print(i) ... Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: 'A' object is not iterable
Интерпретатор говорит, что объект типа A не является итерируемым объектом. Другими словами, цикл for
ожидает, что после in
будет стоять итерируемый объект, а не итератор. Как же так, если цикл for
потом вызывает метод __next__()
, который есть только у итераторов?
На самом деле цикл for
ожидает, что у объекта есть не только метод __next__
, но и __iter__
. Задача метода __iter__
– "превращать" итерируемый объект в итератор. Если в цикл for
передается уже итератор, то метод __iter__()
этого объекта должен возвращать сам объект:
>>> class A: ... def __init__(self, qty): ... self.qty = qty ... def __iter__(self): ... return self ... def __next__(self): ... if self.qty > 0: ... self.qty -= 1 ... return '+' ... else: ... raise StopIteration ... >>> a = A(4) >>> for i in a: ... print(i) ... + + + +
Если циклу for
передается не итератор, а итерируемый объект, то его метод __iter__()
должен возвращать не сам объект, а какой-то объект-итератор. То есть объект, созданный от другого класса.
Получается, в классах-итераторах метод __iter__
нужен лишь для совместимости. Ведь если for
работает как с итераторами, так и итерируемыми объектами, но последние требуют преобразования к итератору, и for
вызывает __iter__()
без оценки того, что ему передали, то требуется, чтобы оба – iterator и iterable – поддерживали этот метод. С точки зрения наличия в классе метода __iter__
итераторы можно считать подвидом итерируемых объектов.
Очевидно, по аналогии с next()
, цикл for
вызывает не метод __iter__()
, а встроенную в Python функцию iter()
.
Если список передать функции iter()
, получим совсем другой объект:
>>> s = [1, 2] >>> si = iter(s) >>> type(s) <class 'list'> >>> type(si) <class 'list_iterator'> >>> si <list_iterator object at 0x7f217a583320> >>> next(si) 1 >>> next(si) 2 >>> next(si) Traceback (most recent call last): File "<stdin>", line 1, in <module> StopIteration
Как видно, объект класса list_iterator
исчерпывается как нормальный итератор. Список s при этом никак не меняется. Отсюда понятно, почему после обхода циклом for
итерируемые объекты остаются в прежнем составе. От них создается "копия"-итератор, а с ними самими цикл for
не работает.
Напишем свой iterable-класс и связанный с ним iterator-класс, чтобы проиллюстрировать, как в Python может быть реализована взаимосвязь между итерируемым объектом и его итератором.
class Letters: def __init__(self, string): self.letters = [] for i in string: self.letters.append(f'-{i}-') def __iter__(self): return LettersIterator(self.letters[:]) class LettersIterator: def __init__(self, letters): self.letters = letters def __iter__(self): return self def __next__(self): if self.letters == []: raise StopIteration item = self.letters[0] del self.letters[0] return item kit = Letters('aeoui') print(kit.letters) for i in kit: print(i) print(kit.letters)
Результат:
['-a-', '-e-', '-o-', '-u-', '-i-'] -a- -e- -o- -u- -i- ['-a-', '-e-', '-o-', '-u-', '-i-']
Заметим, что если в программе определяется класс, от которого будут создаваться итерируемые объекты, то в этой же программе должен быть класс, от которого создаются итераторы для этих объектов. При этом сам по себе класс, порождающий итераторы, никакой ценности может не иметь в том смысле, что непосредственно от него создание экземпляров в основной ветке программы не предполагается.
Обратное не верно. Если нужен класс, от которого создаются итераторы, определять класс для неких связанных с ним итерируемых объектов не требуется.
Практическая работа
Напишите класс-итератор, объекты которого генерируют случайные числа в количестве и в диапазоне, которые передаются в конструктор.
Курс с примерами решений практических работ:
pdf-версия