Итерируемый объект, итератор и генератор в 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.