Итерируемый объект, итератор и генератор в 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__()
, так как они неявно присутствуют у генератора.
Если логика генератора проста, вместо функции можно использовать выражение, создающее генератор:
g = (round(random()+i, 2) for i in range(5))
for i in g:
print(i)
Данный пример не идентичен приведенным выше функции и классу. Здесь целая часть каждого следующего числа больше чем у предыдущего на единицу.
Генераторное выражение и функция-генератор возвращают объект одного и того же типа - generator
.