Итераторы и итерируемые объекты в Python

В английской документации по 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-класс.

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-']

Практическая работа

Напишите класс-итератор, объекты которого генерируют случайные числа в количестве и в диапазоне, которые передаются в конструктор.

Курс с примерами решений практических работ:
android-приложение, pdf-версия

Объектно-ориентированное программирование на Python