Итераторы#
Язык Python является высокоуровневым языком программирования. В нём абсолютно всё является объектом. В связи с этим открывается ряд новых возможностей, среди которых несколько иной подход к реализации циклов.
В отличие от других языков программирования в Python цикл for
работает только с итерируемыми сущностями.
Он не работает, например, с целочисленными счётчиками напрямую, как это происходит в C++, к примеру.
Important
Концепция итератора - это одна из ключевых особенностей Python.
Итерируемый объект - это любой объект, реализующий протокол итератора. Этот протокол лежит в основе любого итерируемого типа: списка, кортежа, словаря, множества, очереди,стека, генератора и др.
Important
В Python вы встретите как итерируемые (Iterable
), так и объекты типа Sequence
.
Тип Sequence
является производным от Iterable
, расширяя последний поддержкой индексации, т.е. к элементам объекта типа Sequence
можно обращаться по индексу.
Классический пример - массивы.
В остальном же для Sequence
справедливо всё то, что справедливо и для Iterable
.
Пример ниже.
# Список поддерживает индексацию,
# т.к. имеет тип Sequence
a_list = [1, 2, 3, 4, 3, 1]
print("a_list[1] =", a_list[1])
a_list[1] = 2
# Подключим функцию cprint для цветного вывода ошибок
from termcolor import cprint
# Но множество индексацию не поддерживает
a_set = set(a_list)
try:
print(a_set[1])
except Exception as ex:
# Попытка обращения по индексу приводит к такому исключению
cprint(f"{ex.__class__.__name__}:", "red", end=" ")
print(ex, "(множество не индексируется)")
---------------------------------------------------------------------------
ModuleNotFoundError Traceback (most recent call last)
Cell In[2], line 2
1 # Подключим функцию cprint для цветного вывода ошибок
----> 2 from termcolor import cprint
4 # Но множество индексацию не поддерживает
5 a_set = set(a_list)
ModuleNotFoundError: No module named 'termcolor'
# Однако и список, и множество могут использоваться в цикле for,
# т.к. реализуют протокол итератора (унаследованы от Iterable)
print("Список:")
for x in a_list:
print(x, end=" ")
print("\nМножество:")
for s in a_set:
print(s, end=" ")
Список:
1 2 3 4 3 1
Множество:
1 2 3 4
Note
Заметьте, что set
имеет исключительно уникальные элементы, поэтому последние два числа (3 и 1) из списка во множестве не присутствуют.
Примеры стандартных функций, возвращающих итерируемый объект#
Одна из наиболее часто используемых функций, которая возвращает итерируемый объект - это range
:
for i in range(5):
print(i, end=" ")
0 1 2 3 4
Important
Это принципиально иной подход к реализации цикла for
в языке.
Хоть i
в данном случае похож на простой счётчик - это не так.
range
возвращает итерируемый объект.
Ни с какими целыми числами цикл for
в Python (напрямую) не работает, в отличие, например, от цикла for
языка C++: for (int i = 0; i < 5; ++i)
.
Целое же значение генерируется или получается при каждой итерации цикла при применении встроенной функции next
к итерируемому объекту (подробности см. Протокол итератора).
Important
Главное преимущество такого подхода - гарантированное исключение ошибки выхода за границы итерируемой сущности (массива, списка и пр.), что является одной из серьёзнейших проблем в низкоуровневых языках.
С помощью range
можно создать список, например, записав списковое выражение, по сути своей являющееся построением списка из генератора (см. раздел о генераторах):
# Либо так (это не путь Python)
a = []
for x in range(5):
a.append(x*x)
# Либо списковым выражением (путь Python)
b = [x*x for x in range(5)]
print(f"a = {a}, b = {b}")
a = [0, 1, 4, 9, 16], b = [0, 1, 4, 9, 16]
Note
Второй способ, к тому же, гораздо быстрее выполняется.
Ещё один пример - функция enumerate
.
Будучи применённой к переданному итерируемому объекту, она возвращает новую итерируемую сущность, которая при итерировании возвращает пару (индекс, элемент)
.
Для начала о том, как делать не стоит:
# Пусть есть некоторый список
cubes = [x**3 for x in range(5)]
# Способ C++, но не Python
for i in range(len(cubes)):
print(f"cubes[{i}] = {cubes[i]}", end="; ")
cubes[0] = 0; cubes[1] = 1; cubes[2] = 8; cubes[3] = 27; cubes[4] = 64;
А вот как делают в Python:
for i, x in enumerate(cubes):
print(f"cubes[{i}] = {x}", end="; ")
cubes[0] = 0; cubes[1] = 1; cubes[2] = 8; cubes[3] = 27; cubes[4] = 64;
Note
Обратите внимание, что enumerate
возвращает кортеж из двух элементов.
В данном случае этот кортеж сразу же распаковывался в цикле for
в две переменные i
и x
.
Последний пример - функция zip
, которая помогает в одном цикле обходить сразу несколько колекций.
Допустим, есть три списка произвольной длины: a
, b
и c
.
Нам нужно вывести сумму соответствующих элементов.
Вот способ низкоуровневых языков:
a = [1, 2] # 2 элемента (самый короткий)
b = [-3, -7, 4, 0] # 4 элемента
c = [2, -1, -4] # 3 элемента
for i in range(len(a)):
print(a[i] + b[i] + c[i])
0
-6
Но почему в range
передана длина именно a
, а не любого другого массива?
В данном случае a
оказался самым коротким из массивов.
Другие массивы не обошлись полностью, но это не так страшно.
Important
Гораздо хуже было бы, окажись a
самым длинным из всех.
Тогда индекс i
вышел бы за границы остальных (коротких) массивов, а это привело бы к исключению и концу работы интерпретатора.
Вот пример, что будет, если в range
передать длину большего списка b
.
try:
for i in range(len(b)):
print(a[i] + b[i] + c[i])
except Exception as ex:
cprint(f"{ex.__class__.__name__}:", "red", end=" ")
print(ex)
0
-6
IndexError: list index out of range
Проверять равенство длин всех массивов перед циклом?
Плохое решение.
Почему?
Функция len
каждый раз пересчитывает длину, проходя массив полностью.
Представьте, что наши три массива содержали бы по миллиону и более элементов, и вопрос отпадёт сам собой.
Функция zip
лишена указаных недостатков.
Если переданные ей массивы имеют разную длину, то возвращать результат она будет до тех пор, пока не закончится самый короткий список (кортеж, массив и др.).
Вот пример:
for ai, bi, ci in zip(a, b, c):
print(ai + bi + ci)
0
-6
Короткий список - a
- имел длину 2, поэтому в результате вывелось 2 числа.
При этом мы не задумывались, какой из массивов короткий, чья длина должна быть передана для обхода по индексу.
Note
Функция zip
принимает неограниченное число аргументов.
Все рассмотренные функции возвращают итерируемые сущности,другими словами, объекты, реализующими протокол итератора. Так что же это за протокол такой?
Протокол итератора#
Рассмотрим, как выполняется цикл for
, шаг за шагом.
В качестве примера возьмём следующий простой цикл:
for i in range(3):
print(i)
0
1
2
Что скрыто от наших глаз:
# В начале инициализируется объект range
r = range(3)
# Затем инициализируется итератор по r,
# в результате получается итерируемый объект
r_iter = iter(r)
# Теперь в каждом шаге цикла for к итератору
# применяется стандартная функция next
# - i = 0
print(next(r_iter))
# - i = 1
print(next(r_iter))
# - i = 2
print(next(r_iter))
0
1
2
Мы достигли конца - значения 2.
Как цикл for
понимает, что пора завершаться?
Если ещё раз применить next
, то мы получит исключение StopIteration
, говорящее о достижении итератором конца итерируемого объекта:
try:
next(r_iter)
except Exception as ex:
cprint(ex.__class__.__name__, "red")
StopIteration
Именно так и работает цикл for
:
Получает итерируемый объект
a
.Преобразует его в итератор
a_iter = iter(a)
.Применяет к нему функцию
next(a_iter)
до тех пор, пока не инициируется исключениеStopIteration
.
В нашем случае объект a
имел тип range
, т.е. был объектом данного класса.
Внутри этого класса определён метод __iter__
(или __getitem__
, если необходимо поддерживать индексацию), который вызывается с помощью iter(a)
и превращает объект a
в итератор a_iter
.
Можно сказать, что iter(a)
то же самое, что и a.__iter__()
.
Три приведённых выше пункта совместно с реализацией метода __iter__
или __getitem__
- это и есть протокол итератора или протокол итерации.
Пример собственного итерируемого класса#
Создадим класс Range
, предназначенный для той же задачи, что и стандартный range
:
class Range:
# Конструктор (инициализатор) объекта данного класса
def __init__(self, start, stop=None, step=1):
if stop is None:
stop = start
start = 0
# Внутри каждого объекта будет лежать
# соответствующий генератор, который
# в отличие от списка не требует выделения
# памяти под каждый элемент коллекции
self.values = range(start, stop, step)
# Функция преобразования объекта данного класса
# в итератор
def __iter__(self):
return iter(self.values)
Note
О генераторах см. главу Генераторы.
Работа с объектами нашего класса ничем не отличается от работы с range
:
for i in Range(-3, 8, 2):
print(i, end=" ")
-3 -1 1 3 5 7
Таким образом мы и реализовали протокол итерации.
Упражнения#
Опишите стандартные
zip
иenumerate
собственными классами по аналогии с классомRange
, рализовав протокол итерации.
См. также#
Более подробно и строго об итераторах и протоколе итерации рассказано здесь.