Итерируемый объект, итератор и генератор в Python

В Python итерируемый объект (iterable или iterable object), итератор (iterator или iterator object) и генератор (generator или generator object) - разные понятия, а не синонимы одного и того же. От итерируемого объекта можно получить его "копию"-итератор; генератор является разновидностью итератора.

В некоторых источниках итератор рассматривается как частный случай итерируемого объекта, поскольку оба поддерживают операцию итерации, то есть обход циклом for. Однако for работает только с итераторами. Переданный на обработку объект должен иметь метод __iter__(), который for неявно вызывает перед обходом. Метод __iter__() должен возвращать итератор.

У итерируемого объекта, то есть объекта, который можно "превратить" в итератор, должен быть метод __iter__(), который возвращает соответствующий объект-итератор.

>>> a = [1, 2]
>>> b = a.__iter__() 
>>> a
[1, 2]
>>> b
<list_iterator object at 0x7f7e24c1abe0>
>>> type(a)
<class 'list'>
>>> type(b)
<class 'list_iterator'>

У итерируемого объекта нет метода __next__(), который используется при итерации:

>>> a.__next__()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'list' object has no attribute '__next__'

У итератора есть метод __next__(), который извлекает из итератора очередной элемент. При этом этот элемент уже не содержится в итераторе. Таким образом, итератор в конечном итоге опустошается:

>>> b.__next__()
1
>>> b.__next__()
2
>>> b.__next__()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

Метод __next__() исчерпанного итератора возбуждает исключение StopIteration.

У итераторов, также как у итерируемых объектов, есть метод __iter__(). Однако в данном случае он возвращает сам объект-итератор:

>>> a = [1, 2]
>>> a = "hi"
>>> b = a.__iter__()
>>> c = b.__iter__()
>>> a
'hi'
>>> b
<str_iterator object at 0x7f7e24c1ad30>
>>> c
<str_iterator object at 0x7f7e24c1ad30>
>>> b.__next__()
'h'
>>> c.__next__()
'i'
>>> b.__next__()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

Здесь переменные b и c указывают на один и тот же объект.

Примеры итерируемых объектов в Python - список, словарь, строка и другие контейнерные типы (они же коллекции), тип, возвращаемый функцией range(). 

Примеры итераторов - файловые объекты, генераторы, итераторы созданные на основе списка, строки, объекта типа range и т. д.

В Python есть встроенные функции iter() и next(), которые соответственно вызывают методы __iter__() и __next__() объектов, переданных в качестве аргумента.

>>> a = {1: 'a', 2: 'b'}
>>> b = iter(a)
>>> b
<dict_keyiterator object at 0x7f7e24c17778>
>>> next(b)
1

Внутренний механизм цикла for сначала вызывает метод __iter__() объекта. Так что, если передан итерируемый объект, создается итератор. После этого применяется метод __next__() до тех пор, пока не будет возбуждено исключение StopIteration.

Поскольку метод __iter__() итератора возвращает сам итератор, то после перебора циклом for объект исчерпывается. То есть получить данные из итератора можно только один раз. В случае с коллекциями это не так. Здесь создается другой объект - итератор. Он, а не итерируемый объект, отдается на обработку циклу for.

>>> a = range(2)
>>> b = iter(a)
>>> type(a)
<class 'range'>
>>> type(b)
<class 'range_iterator'>
>>> for i in a:
...     print(i)
... 
0
1
>>> for i in a:
...     print(i)
... 
0
1
>>> for i in b:
...     print(i)
... 
0
1
>>> for i in b:
...     print(i)
... 
>>> 

Отличительной особенностью генераторов является то, что они создаются не на основе классов, а путем вызова функции, содержащей инструкцию yield, или специальным генераторным выражением по синтаксису похожим на генератор списка. Отметим, генератор списка, который является особым выражением, к генераторам, которые являются разновидностью объектов-итераторов, отношения не имеет. Подробнее можно почитать здесь

Другими словами, если потребуется создать свой итератор, может оказаться проще определить функцию с yield или воспользоваться выражением, чем создавать класс с методами __next__() и __iter__().

Рассмотрим пример. Определим сначала собственный класс-итератор:

from random import random
 
 
class RandomIncrease:
    def __init__(self, quantity):
        self.qty = quantity
        self.cur = 0
 
    def __iter__(self):
        return self
 
    def __next__(self):
        if self.qty > 0:
            self.cur += random()
            self.qty -= 1
            return round(self.cur, 2)
        else:
            raise StopIteration
 
 
iterator = RandomIncrease(5)
for i in iterator:
    print(i)

Пример выполнения кода:

0.65
1.17
1.19
1.45
2.11

Наш итератор выдает числа по нарастающей. При этом каждое следующее число больше предыдущего на случайную величину.

Здесь же отметим преимущество итераторов как таковых перед контейнерными типами вроде списков. В памяти компьютера не хранятся все элементы итератора, в основном лишь описание, как получить следующий элемент. Если представить, что нужны тысячи чисел или надо генерировать сложные объекты, выгода существенна.

В случае с функцией, создающей генератор, приведенный выше пример может выглядеть так:

def random_increase(quantity):
    cur = 0
    while quantity > 0:
        cur += random()
        quantity -= 1
        yield round(cur, 2)
 
 
generator = random_increase(5)
for i in generator:
    print(i)

Нам незачем самим определять методы __iter__() и __next__(), так как они неявно присутствуют у генератора.

Если логика генератора проста, вместо функции можно использовать выражение, создающее генератор:

generator = (round(random()+i, 2) for i in range(5))
 
for i in generator:
    print(i)

Данный пример не идентичен приведенным выше функции и классу. Здесь целая часть каждого следующего числа больше чем у предыдущего на единицу.

Генераторное выражение и функция-генератор возвращают объект одного и того же типа - generator.