4137

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

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

Итератор Python

Итератор — это механизм поэлементного обхода данных. Фактически, он представляет собой объект, который является результатом вызова метода __iter__ итерируемого объекта. Его основная задача заключается в отслеживании следующего элемента в последовательности. Другими словами, итератор «знает» какой элемент в последовательности будет следующим, и может обрабатывать такие элементы по одному.

В Python существует два вида итераторов:

  • внешний — это классический, он же pull-based итератор, используемый в случаях, когда процессом обхода элементов последовательности явно управляет клиент, используя метод __next__;
  • внутренний — это push-based итератор, в который передается callback-функция. Он не управляется клиентом, а передает ему уведомление о получении следующего элемента.

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

 

Логику работы итератора можно продемонстрировать схематически:

image15

 

Обозначения блоков:

  • Aggregate — это итерируемый объект по которому может перемещаться итератор.
  • Iterator — интерфейс самого итератора.
  • ConcreteAggregate — определенная реализация агрегата.
  • ConcreteIterator — реализация итератора для конкретного агрегата.
  • Client — клиентский интерфейс, который использует Aggregate и итератор для его обхода.

Пример реализации итератора

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

В качестве примера реализуем классический итератор.

Абстрактные классы:

class Aggregate(abc.ABC):

    @abc.abstractmethod
    def iterator(self):
        """
        Возвращает итератор
        """
        pass


class Iterator(abc.ABC):
    def __init__(self, collection, cursor):
        self._collection = collection
        self._cursor = cursor

    @abc.abstractmethod
    def first(self):
        """
        Возвращает итератор к началу агрегата.
        Так же называют reset
        """
        pass

    @abc.abstractmethod
    def next(self):
        """
        Переходит на следующий элемент агрегата.
        Вызывает ошибку StopIteration, если достигнут конец последовательности.
        """
        pass

    @abc.abstractmethod
    def current(self):
        """
        Возвращает текущий элемент
        """
        pass

Реализация итератора для списка:

class ListIterator(Iterator):
    def __init__(self, collection, cursor):
        """
        :param collection: список
        :param cursor: индекс с которого начнется перебор коллекции.
        так же должна быть проверка -1 >= cursor < len(collection)
        """
        super().__init__(collection, cursor)

    def first(self):
        """
        Начальное значение курсора -1.
        Так как в нашей реализации сначала необходимо вызвать next 
         который сдвинет курсор на 1.
        """
        self._cursor = -1

    def next(self):
        """
        Если курсор указывает на послений элемент, то вызываем StopIteration,
        иначе сдвигаем курсор на 1
        """
        if self._cursor + 1 >= len(self._collection):
            raise StopIteration()
        self._cursor += 1

    def current(self):
        """
        Возвращаяем текущий элемент
        """
        return self._collection[self._cursor]

Реализация агрегата:

class ListCollection(Aggregate):
    def __init__(self, collection):
        self._collection = list(collection)

    def iterator(self):
        return ListIterator(self._collection, -1)

Дальше мы можем создать объект коллекции, а затем обойти все его элементы с использованием итератора.

Для этого пишем такой код:

collection = (1, 2, 5, 6, 8)
aggregate = ListCollection(collection)
itr = aggregate.iterator()

# обход коллекции
while True:
    try:
        itr.next()
    except StopIteration:
        break
    print(itr.current())

Поскольку мы создали метод first, сбрасывающий итератор в начальное состояние, то можем использовать этот итератор снова:

# возвращаем итератор в исходное состояние
itr.first()

while True:
    try:
        itr.next()
    except StopIteration:
        break
    print(itr.current())

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

Итерируемый объект можно описать как последовательность элементов, которая может быть представлена в любом виде: файлом, строкой, списком, кортежем или любой другой структурой данных, элементы которой можно перебирать. 

По сути, итерируемыми объектами являтся все объекты, от которых встроенная функция iter() может получить оператор. 

Важно отметить, что это могут быть не только те объекты, которые реализуют метод __iter__. Дело в том, что для получения итератора функция iter() в первую очередь вызывает метод __iter__, а если он не реализован — проверяет наличие метода __getitem__ и уже на его основе создает итератор. При этом TypeError вызывается только в том случае, когда в объекте не реализован ни один из этих методов.

В языке программирования Python итерируемые объекты представлены классом collections.abc.Iterator:

class Iterator(Iterable):

    __slots__ = ()

    @abstractmethod
    def __next__(self):
        'Return the next item from the iterator. When exhausted, raise StopIteration'
        raise StopIteration

    def __iter__(self):
        return self

    @classmethod
    def __subclasshook__(cls, C):
        if cls is Iterator:
            return _check_methods(C, '__iter__', '__next__')
        return NotImplemented

Здесь стоит дать некоторые объяснения:

  • __next__ — возвращает следующий доступный в последовательности элемент, а если он не было обнаружен, то возвращает исключение StopIteration.
  • __iter__ — возвращает элемент self, что позволяет использовать итератор там, где ожидается работа с итерируемым объектом, например в цикле for.
  • __subclasshook__ — проверяет класс на наличие методов __iter__ и __next__.

Подводя черту можно сказать, что итератор в языке Python — это любой объект, который реализует метод __next__ без аргументов и возвращает следующий элемент последовательности или ошибку StopIteration, сигнализирующую об окончании последовательности. При этом он сам реализует метод __iter__ из-за чего сам является итерируемым объектом.

В качестве примера создадим итерируемый объект на основе списка, а также его итератор:

class ListIterator(collections.abc.Iterator):
    def __init__(self, collection, cursor):
        self._collection = collection
        self._cursor = cursor

    def __next__(self):
        if self._cursor + 1 >= len(self._collection):
            raise StopIteration()
        self._cursor += 1
        return self._collection[self._cursor]

class ListCollection(collections.abc.Iterable):
    def __init__(self, collection):
        self._collection = collection

    def __iter__(self):
        return ListIterator(self._collection, -1)

Варианты работы:

collection = [1, 2, 5, 6, 8]
aggregate = ListCollection(collection)

for item in aggregate:
    print(item)

print("*" * 50)

itr = iter(aggregate)
while True:
    try:
        print(next(itr))
    except StopIteration:
        break

Чтобы по окончании итерации функция next() не возвращала ошибку StopIteration, мы можем передать в нее второй аргумент:

itr = iter(aggregate)
while True:
    item = next(itr, None)
    if item is None:
        break
    print(item)

Также стоит добавить, что встроенную функцию iter() можно вызывать с двумя аргументами, что позволит создать итератор из вызываемого объекта. В таком случае первый аргумент является вызываемым объектом, а второй выступает в роли ограничителя.

Например, из функции возвращающей рандомные значения от 1 до 6 мы можем сделать итератор, который будет возвращать значения, до тех пор, пока это не окажется цифра 6:

Пример кода:

from random import randint

def d6():
    return randint(1, 6)

for roll in iter(d6, 6):
    print(roll)

Генераторы в Python

Генераторы по своей сути являются теми же итераторами, только с их помощью итерировать объект можно всего один раз. Это связано с тем, что они не хранят полученные значения в памяти, а генерируют элементы «на лету». 

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

В качестве примера рассмотрим простую функцию-генератор:

def generator_function():
    for i in range(10):
        yield i
        
for item in generator_function():
    print (item)
    
# Вывод: 0
# 1
# 2
# 3
# 4
# 5
# 6
# 7
# 8
# 9

Основной плюс генераторов заключается в очень низком потреблении ресурсов. Благодаря этому их часто используют для расчета больших наборов результатов, где выделение памяти для одновременного хранения всех результатов нецелесообразно. 

 

Чтобы стало понятно о чем идет речь, создадим генератор, считающий числа Фибоначчи:

# generator version
def fibon (n):
    a = b = 1
    for i in range(n):
        yield a
        a, b = b, a + b

Даже если в качестве значения n мы поставим большое число, например 1000000, то используя генератор мы можем не переживать за чрезмерное потребление ресурсов, и это не повлияет на работу остального кода.

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

 

Выглядеть это будет так:

def fibon(n):
    a = b = 1
    result = []
    for i in range(n):
        result.append(a)
        a, b = b, a + b
    return result

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

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

Для этого в качестве примера напишем такой код:

def gen_fun():
    print('block 1')
    yield 1
    print('block 2')
    yield 2
    print('end')

for i in gen_fun():
    print(i)

# block 1
# 1
# block 2
# 2
# end

Работа этого генератора будет происходить следующим образом:

  1. Первым делом при вызове gen_fun создается объект-генератор.
  2. Цикл for вызывает функцию iter() с объектом gen_fun и получает итератор этого генератора.
  3. Цикл вызывает функцию next(), которая будет перебирать элементы объекта до тех пор, пока не получит ответ StopIteration.

При этом при каждом вызове next(), выполнение функции продолжается с того момента, где было завершено в последний раз и продолжается до следующего yield

Генераторное выражение

Генераторное выражение это упрощенный с точки зрения синтаксиса способ создать генератор, не определяя и не вызывая функцию. Такой подход удобно использовать для генерации коллекций и их несложных преобразований. 

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

[x * x for x in range(10)]
# [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
[x * x for x in range(10)]
# <generator object <genexpr> at 0x7fe76f7e5db0>

Как видим, в первом случае код генерирует диапазон чисел, а во втором — создает объект generator object, который является итератором. Таким образом мы можем отложить вычисление элементов последовательности до тех пор, пока в них не возникнет необходимость, чем опять же снижаем нагрузку на ресурсы.

При этом стоит учитывать, что генераторные выражения — это в первую очередь выражения, со всеми вытекающими ограничениями.

Изучение Python в SpaceLAB

Лаборатория SpaceLAB — это онлайн-школа, где вы можете бесплатно освоить востребованную IT-профессию с перспективой дальнейшего трудоустройства. На курсе Python вы не только освоите теоретическую часть, но и погрузитесь в реализацию практических задач под кураторством опытных менторов — действующих разработчиков компании AVADA-MEDIA.