Как устроены итераторы

Введение

Во многих языках цикл for не обходится без индексов. Например, в JavaScript, популярный способ обойти массив таков:

var books = ["Code Complete", "Programming Pearls", "The Mythical Man-Month"];
var booksArrayLength = books.length;
for (var i = 0; i < booksArrayLength; i++) {
    console.log(myStringArray[i]);
}

Однако в Python мы делаем так:

for book in books:
    print(book)

Такой более короткой записью мы обязаны итераторам. Далее мы разберемся, что это такое, как они работают и как сделать свой.

Как это устроено

Рассмотрим пример итерации по элементам списка:

>>> books = ["Code Complete", "Programming Pearls", "The Mythical Man-Month"]
>>> for book in books:
...  print(book)
... 
Code Complete
Programming Pearls
The Mythical Man-Month

Чтобы разобраться, как этот цикл работает, избавимся от оператора in:

>>> books_iterator = iter(books)
>>> while True:
...  try:
...   book = next(books_iterator)
...  except StopIteration:
...   break
...  print(book)
... 
Code Complete
Programming Pearls
The Mythical Man-Month

Без оператора in нам пришлось использовать встроенные функции iter и next. Мы к ним еще вернемся, а пока избавимся и от них:

>>> books_iterator = books.__iter__()
>>> while True:
...  try:
...   book = books_iterator.__next__()
...  except StopIteration:
...   break
...  print(book)
... 
Code Complete
Programming Pearls
The Mythical Man-Month

Теперь мы видим: у типа list есть метод __iter__, который возвращает объект. Этот объект, в свою очередь, имеет метод __next__, который возвращает, по одному, элементы списка books и поднимает StopIteration, когда этих элементов больше нет. Еще этот объект тоже имеет метод __iter__, который возвращает его самого:

>>> books_iterator.__iter__() is books_iterator
True

Вместе __iter__ и __next__ составляют протокол итератора. Так вот, объект класса, соблюдающего этот протокол, называется итератором (iterator). Объект класса, который реализует метод __iter__, называется итерируемым (iterable). В данном случае books_iterator -- итератор, books -- итерируемое. Роль метода __next__ заключается в том, чтобы задавать порядок обхода итерируемого (не обязательно делать этот порядок строго от первого к последнему). Метод __iter__ нужен для того, чтобы итератор и итерируемое могли использоваться с оператором in:

>>> books_iterator = iter(books)
>>> for book in books_iterator:
...  print(book)
... 
Code Complete
Programming Pearls
The Mythical Man-Month

Встроенные функции iter и next вызывают методы __iter__ и __next__, проделывая при этом дополнительную работу. Например, iter поднимает TypeError, если __iter__ возвращает не итератор.

Как это сделать

Вот так выглядит итератор, который обходит список, начиная с последнего элемента и заканчивая первым:

class ReverseIt():
    def __init__(self, reverse_me):
        self.reverse_me = reverse_me
        self.current_index = len(reverse_me) - 1

    def __iter__(self):
        return self

    def __next__(self):
        if self.current_index < 0:
            raise StopIteration()
        current_element = self.reverse_me[self.current_index]
        self.current_index -= 1
        return current_element

И вот так его можно использовать:

>>> reverse_books_iterator = ReverseIt(books)
>>> for book in reverse_books_iterator:
...  print(book)
... 
Code Complete
Programming Pearls
The Mythical Man-Month

Мы написали именно reverse_books_iterator = ReverseIt(books), а не reverse_books_iterator = iter(books). Последнее вернет нам обычный итератор. Но мы могли бы так написать, если бы у списка метод __iter__ был определен так:

def __iter__(self):
    return ReverseIt(self)

Резюме

  • Итератор -- объект, у которого есть методы __iter__ и __next__.
  • Итерируемое -- объект, у которого есть метод __iter__ и он возвращает итератор.
  • Эти методы называют "магическими", они часто используются в синтаксических конструкциях вроде for ... in ....

Дальнейшее чтение