Pixamp

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

25.02.2019 23:05 CPython 3.7.2

The article image

На момент написания данной статьи только на одной странице Built-in Types официальной документации Python 3 слово “iterator” встречается 21 раз, а “iterable” – 39. Довольно популярные слова, не так ли? Сегодня мы попытаемся разобраться, что же они значат: что такое итератор (iterator) и что такое итерируемый объект (iterable), а также в чем между ними разница.

Итераторы

Итак, итератор – это такой объект, который представляет некий поток данных (поток чисел, строк и других объектов). Возвращает итератор по одному элементу этого потока за раз. Довольно часто итераторы используются неявно в циклах for.

Рассмотрим такой пример:

>>> companies = ['Amazon', 'Apple', 'Facebook', 'Google', 'Microsoft']
>>> for company in companies:
...     print(company)
...
Amazon
Apple
Facebook
Google
Microsoft

У нас есть список companies, содержащий наименования пяти компаний. Я использую цикл for, который позволяет за пять итераций вывести названия всех компаний в списке. Это и есть пример неявного использования итератора: от нас скрыт процесс получения и использования объекта итератора. Между тем, именно итератор возвращает на каждом этапе название очередной компании.

Впрочем, мы можем получить тот же результат и без использования for:

>>> companies = ['Amazon', 'Apple', 'Facebook', 'Google', 'Microsoft']
>>> companies_iter = iter(companies)
>>> company = next(companies_iter)
>>> print(company)
Amazon
>>> company = next(companies_iter)
>>> print(company)
Apple
>>> company = next(companies_iter)
>>> print(company)
Facebook
>>> company = next(companies_iter)
>>> print(company)
Google
>>> company = next(companies_iter)
>>> print(company)
Microsoft

Здесь мы видим все тот же список companies, содержащий пять компаний. Да и результат работы такой же, как в предыдущем примере. Разница только в том, что здесь я явно получаю и использую объект итератора. Чтобы получить этот объект, я использую функцию iter(). Ей я передаю список companies. В ответ iter() возвращает объект итератора. Ссылка на этот объект присваивается переменной companies_iter. Как мы помним из определения – итератор должен возвращать нам по одному элементу данных. Он и делает это каждый раз, когда мы просим его об этом. Для этого существует функция next(). Всякий раз, когда мы вызываем данную функцию, передавая ей объект итератора, она возвращает нам один элемент данных. Вызвав next() пять раз, я получил все названия компаний списка companies.

Но если сейчас вызвать next() шестой раз, то мы получим исключение StopIteration:

>>> company = next(companies_iter)
Traceback (most recent call last):
    ...
StopIteration

При использовании for также будет получен объект итератора, который затем шаг-за-шагом истощается (подобно тому, как это делал я, используя функцию next()) до тех пор, пока не будет перехвачено то самое исключение StopIteration, генерируемое итератором, когда возвращать больше нечего. StopIteration – это признак того, что итератор истощен.

Чтобы понять лучше, как работает for в данной ситуации, напишем его аналог, используя while:

>>> companies = ['Amazon', 'Apple', 'Facebook', 'Google', 'Microsoft']
>>> companies_iter = iter(companies)
>>> while True:
...     try:
...         company = next(companies_iter)
...         print(company)
...     except StopIteration:
...         break
...
Amazon
Apple
Facebook
Google
Microsoft

Здесь мы видим “бесконечный” цикл while, который выполняется до тех пор, пока не будет перехвачено исключение StopIteration в блоке try/except. В теле цикла вызывается функция next(), которой передается полученный ранее итератор (companies_iter). На каждой итерации цикла мы получаем одно значение итератора, которое затем передается print(). Подобным образом работает и for в первом примере.


Как и список, другие встроенные типы последовательностей (кортеж, строка и т.п.) также могут вернуть итератор:

>>> type(iter((1, 2, 3)))
<class 'tuple_iterator'>
>>> type(iter('abc'))
<class 'str_iterator'>

Множества и словари также возвращают объект итератора:

>>> type(iter({1, 2, 3}))
<class 'set_iterator'>
>>> type(iter({1: 'a', 2: 'b', 3: 'c'}))
<class 'dict_keyiterator'>

Магические методы __iter__() и __next__()

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

Следующий вопрос заключается в том, а что, собственно говоря, происходит, когда мы вызываем функцию next(), передавая ей ранее полученный итератор? В этом случае, вполне ожидаемо, тоже вызывается магический метод – метод объекта итератора __next__().

Чтобы понять это лучше, давайте попробуем создать собственный итератор. Предположим, что у нас есть некая база данных SQLite, одной из таблиц которой является таблица companies, содержащая записи различных компаний:

name address
Amazon.com, Inc. 410 Terry Ave North, Seattle, Washington, U.S.
Apple Inc. 1 Apple Park Way, Cupertino, California, U.S.
Facebook, Inc. 1601 Willow Road, Menlo Park, California, U.S.
Google LLC 1600 Amphitheatre Parkway, Mountain View, California, U.S.
Microsoft Corporation One Microsoft Way, Redmond, Washington, U.S.

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

Сделаем первые наброски. Чтобы иметь возможность быстро тестировать написанный код, мы будем создавать в памяти временную базу данных (БД) с минимальным набором данных, не касаясь реальной БД:

import sqlite3

class Companies:
    def __init__(self, connection):
        self.connection = connection

if __name__ == '__main__':
    # Кортеж с тестовыми данными
    COMPANIES = (
        ('Amazon.com, Inc.', '410 Terry Ave North, Seattle, Washington, U.S.'),
        ('Apple Inc.', '1 Apple Park Way, Cupertino, California, U.S.'),
        ('Facebook, Inc.', '1601 Willow Road, Menlo Park, California, U.S.'),
        ('Google LLC', '1600 Amphitheatre Parkway, Mountain View, California, U.S.'),
        ('Microsoft Corporation', 'One Microsoft Way, Redmond, Washington, U.S.'),
    )

    # Создаем БД в памяти, а также открываем соединение с данной БД
    connection = sqlite3.connect(':memory:')

    # Cоздаем тестовую таблицу "companies"
    connection.execute('CREATE TABLE companies (name, address)')

    # Добавляем тестовые записи в "companies"
    connection.executemany('INSERT INTO companies VALUES (?, ?)', COMPANIES)

    # Создаем экземпляр класса Companies и присваиваем ссылку на него companies
    companies = Companies(connection)

    # Закрываем соединение с БД
    connection.close()

Для работы с базой данных я использую модуль стандартной библиотеки Python sqlite3. Открытое соединение с БД передается при создании экземпляра класса Companies (это соединение пригодится нам чуть позже). Сама база данных создается в памяти, а в таблицу companies добавляются несколько тестовых записей (кортеж COMPANIES).

Объект companies в дальнейшем будет возвращать итератор, но пока абсолютно бесполезен.

Сохраним наш код в файл “companies.py”. Уже сейчас мы можем импортировать класс Companies и использовать его в других модулях (например так: from companies import Companies). В этом случае код блока условия (if __name__ == '__main__') не будет выполняться. Выполняется он только непосредственно при запуске модуля (например вот так: python3 companies.py).

Примечание

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

Напишем теперь класс итератора. Напомню, что мы должны реализовать метод __next__(). Он будет возвращать по одной записи таблицы базы данных за вызов, а когда записи закончатся – сгенерирует исключение StopIteration. Формально итератор также должен иметь метод __iter__(), поэтому мы реализуем и его, хоть он и не будет использоваться в нашем примере. Как я писал выше, ожидается, что __iter__() должен вернуть объект итератора, но так как это и есть итератор, то метод просто будет возвращать ссылку на сам экземпляр класса. Также, для удобства представления данных компаний, добавим класс данных Company.

Не по теме

Классы данных или data classes появились в Python начиная с версии 3.7. Подробно про них мы поговорим в одной из следующих статей. Вместо класса данных я мог бы использовать namedtuple или, например, обычный словарь. Впрочем, лучшим решением, в данной ситуации, было бы использовать атрибут объекта соединения row_factory. По умолчанию методы fetchone(), fetchmany() и fetchall() возвращают кортеж, но это можно изменить с помощью row_factory. Мы можем написать собственную фабрику, которая будет возвращать записи в нужном нам виде. Также можно использовать готовую фабрику Row. В обоих случаях реализовывать собственный итератор нет необходимости, так как объект курсора сам является итератором.

Итак, вот что получилось:

from dataclasses import dataclass

@dataclass
class Company:
    name: str
    address: str

class CompaniesIterator:
    def __init__(self, connection):
        self.cursor = connection.execute('SELECT * FROM companies')

    def __iter__(self):
        return self

    def __next__(self):
        row = self.cursor.fetchone()
        if row is None:
            raise StopIteration
        return Company(name=row[0], address=row[1])

Примечание

Метод fetchone() при каждом вызове возвращает следующую запись результата запроса, либо None, если возвращать больше нечего.

Теперь нам нужно сделать так, чтобы экземпляр Companies мог вернуть итератор. Для этого нужно реализовать метод __iter__():

class Companies:
    def __init__(self, connection):
        self.connection = connection

    def __iter__(self):
        return CompaniesIterator(self.connection)

И все вместе:

from dataclasses import dataclass
import sqlite3

@dataclass
class Company:
    name: str
    address: str

class CompaniesIterator:
    def __init__(self, connection):
        self.cursor = connection.execute('SELECT * FROM companies')

    def __iter__(self):
        return self

    def __next__(self):
        row = self.cursor.fetchone()
        if row is None:
            raise StopIteration
        return Company(name=row[0], address=row[1])

class Companies:
    def __init__(self, connection):
        self.connection = connection

    def __iter__(self):
        return CompaniesIterator(self.connection)

if __name__ == '__main__':
    COMPANIES = (
        ('Amazon.com, Inc.', '410 Terry Ave North, Seattle, Washington, U.S.'),
        ('Apple Inc.', '1 Apple Park Way, Cupertino, California, U.S.'),
        ('Facebook, Inc.', '1601 Willow Road, Menlo Park, California, U.S.'),
        ('Google LLC', '1600 Amphitheatre Parkway, Mountain View, California, U.S.'),
        ('Microsoft Corporation', 'One Microsoft Way, Redmond, Washington, U.S.'),
    )

    connection = sqlite3.connect(':memory:')

    connection.execute('CREATE TABLE companies (name, address)')
    connection.executemany('INSERT INTO companies VALUES (?, ?)', COMPANIES)

    companies = Companies(connection)

    for company in companies:
        print(company.name)

    connection.close()

Результат работы будет таким:

Amazon.com, Inc.
Apple Inc.
Facebook, Inc.
Google LLC
Microsoft Corporation

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

Объект, на который ссылается companies, является итерируемым объектом. Объект называется итерируемым, если у него есть метод __iter__(), возвращающий итератор. Другой признак итерируемого объекта – наличие метода __getitem__(). Этот метод используется в ситуациях, когда происходит обращение по ключу (object[key]). Итерируемые объекты могут иметь либо __iter__(), либо __getitem__(), либо оба метода сразу. Отсюда следует то, что любой итератор также является итерируемым объектом (так как у итератора есть метод __iter__()), но не наоборот. Все вышеупомянутые встроенные типы данных, такие как список, кортеж, словарь и другие – являются итерируемыми объектами.

Стоит заметить, что создавать отдельный класс итератора вовсе необязательно. Ничего не мешает нам совместить классы Companies и CompaniesIterator:

class Companies:
    def __init__(self, connection):
        self.connection = connection

    def __iter__(self):
        self.cursor = self.connection.execute('SELECT * FROM companies')
        return self

    def __next__(self):
        row = self.cursor.fetchone()
        if row is None:
            raise StopIteration
        return Company(name=row[0], address=row[1])

Теперь класс CompaniesIterator нам больше не нужен, так как Companies сам реализует протокол итератора. Сейчас экземпляр класса Companies будет как итерируемым объектом, так и итератором.

Магический метод __getitem__()

Итак, мы рассмотрели пример использования итерируемого объекта с методом __iter__(). Для полноты картины, ниже я привожу другой пример, но уже с __getitem__():

class MyList:
    def __init__(self, initlist=None):
        self.data = []
        if initlist is not None:
            self.data[:] = initlist

    def __getitem__(self, i):
        return self.data[i]

mylist = MyList([1, 2, 3])

for item in mylist:
    print(item)

Результат работы:

1
2
3

MyList - это по сути класс-обертка (с очень ограниченным функционалом) для списка. Обратите внимание на метод __getitem__(): он возвращает i-ый элемент списка self.data. Стоит заметить, что for также прекрасно справляется и с таким типом итерируемого объекта. Это потому, что функции iter(), которую и использует for, можно передать любой итерируемый объект. Такой объект может иметь либо __iter__(), либо __getitem__(), либо оба метода сразу.

В случае с нашим MyList мы также получим объект итератора, используя iter():

>>> mylist = MyList([1, 2, 3])
>>> mylist_iter = iter(mylist)
>>> type(mylist_iter)
<class 'iterator'>
>>> next(mylist_iter)
1

Встроенная функция iter()

Как уже упоминалось выше, итерируемый объект может иметь оба метода сразу. Давайте добавим метод __iter__() к уже реализованному ранее __getitem__():

class MyList:
    def __init__(self, initlist=None):
        self.data = []
        if initlist is not None:
            self.data[:] = initlist

    def __iter__(self):
        print('__iter__() is called')
        return iter(self.data)

    def __getitem__(self, i):
        print('__getitem__() is called')
        return self.data[i]

Что произойдет, если сейчас попробовать вызвать встроенную функцию iter() для экземпляра класса MyList? Какой из методов будет выбран?

Давайте проверим:

>>> mylist = MyList([1, 2, 3])
>>> for item in mylist:
...     item
...
__iter__() is called
1
2
3

Как мы видим, функция iter() отдает предпочтение __iter__().

Итак, можно выделить несколько этапов работы iter():

  1. Сначала проверяется наличие метода __iter__ () у переданного объекта. Если метод присутствует, то он вызывается для того, чтобы получить объект итератора, затем iter() завершает работу, возвращая полученный итератор.
  2. Если __iter__ () отсутствует, то проверяется наличие метода __getitem__(). Если данный метод реализован, то создается итератор, который будет поочереди возвращать элементы переданного объекта, используя __getitem__(). В этом случае происходит обращение по индексу начиная с нуля (object[0], object[1], object[2] и так далее), пока не будет перехвачено исключение IndexError. iter() завершает работу, возвращая созданный итератор.
  3. Если же оба метода отсутствуют, то будет сгенерировано исключение TypeError.

Дополнительный материал не по теме

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

class Companies:
    def __init__(self, connection):
        self.connection = connection

    def __iter__(self):
        return self.connection.execute('SELECT * FROM companies')

А используя атрибут объекта соединения row_factory, можно избавиться и от класса Company. Мы можем воспользоваться существующей фабрикой Row:

connection.row_factory = sqlite3.Row

Единственная проблема теперь заключается в том, что у company больше нет атрибута name. При запуске программы будет сгенерировано исключение AttributeError. В данной ситуации нам нужно либо заменить company.name на company['name'] (Row поддерживает обращение по ключу), либо реализовать свою фабрику, например, на базе того же Row:

import sqlite3

class MyRow(sqlite3.Row):
    def __getattr__(self, name):
        return self[name]

class Companies:
    def __init__(self, connection):
        self.connection = connection

    def __iter__(self):
        return self.connection.execute('SELECT * FROM companies')

if __name__ == '__main__':
    COMPANIES = (
        ('Amazon.com, Inc.', '410 Terry Ave North, Seattle, Washington, U.S.'),
        ('Apple Inc.', '1 Apple Park Way, Cupertino, California, U.S.'),
        ('Facebook, Inc.', '1601 Willow Road, Menlo Park, California, U.S.'),
        ('Google LLC', '1600 Amphitheatre Parkway, Mountain View, California, U.S.'),
        ('Microsoft Corporation', 'One Microsoft Way, Redmond, Washington, U.S.'),
    )

    connection = sqlite3.connect(':memory:')
    connection.row_factory = MyRow

    connection.execute('CREATE TABLE companies (name, address)')
    connection.executemany('INSERT INTO companies VALUES (?, ?)', COMPANIES)

    companies = Companies(connection)

    for company in companies:
        print(company.name)

    connection.close()

Теперь все в порядке! Мы можем и дальше расширять наш класс Companies, добавляя нужный функционал. Например, для того, чтобы узнать, сколько всего записей в таблице, было бы удобно использовать стандартную функцию Python len():

class Companies:
    def __init__(self, connection):
        self.connection = connection

    def __iter__(self):
        return self.connection.execute('SELECT * FROM companies')

    def __len__(self):
        cursor = connection.execute('SELECT count(*) FROM companies')
        return cursor.fetchone()[0]

Теперь мы можем легко получить информацию о количестве записей:

>>> len(companies)
5

Дата последнего редактирования статьи: 06.06.2019 12:45

Следующая статья › Генераторы в Python
Предыдущая статья › Операции над списками в Python

Главная страница