Итераторы

Итераторы позволяют обойти все элементы итерируемых объектов и по всюду используются в python.

Note

Итератор (iterator) — это объект, который должен определить метод __next__ (т.е. реагировать на функцию next), выдавать следующий элемент коллекции по вызову этого метода и бросать исключение StopIteration, когда эти элементы исчерпались.

Получить стандартный итератор по коллекции можно встроенной функцией iter.

a_list = list(range(3))
it = iter(a_list)
print(it)
<list_iterator object at 0x00000217B79E9610>

В ячейке выше объявлен список a_list = [0, 1, 2] и итератор it по нему. Теперь у этого итератора можно спросить следующий элемент списка функцией next.

a = next(it)
print(a)
b = next(it)
print(b)
c = next(it)
print(c)


d = next(it)
0
1
2
---------------------------------------------------------------------------
StopIteration                             Traceback (most recent call last)
Input In [2], in <cell line: 9>()
      5 c = next(it)
      6 print(c)
----> 9 d = next(it)

StopIteration: 

В ячейке выше функция next применяется к итератору it 4 раза. Первые 3 из них итератор успешно возвращают элементы списка x по порядку. На четвертом запросе следующего элемента в списке x уже нет, итератор бросил исключение StopIteration.

Вообще говоря, можно изменить список при итерации по нему, но желательно по возможности избегать этого.

it = iter(a_list)
a = next(it)

a_list.insert(0, 42)
b, c, d = next(it), next(it), next(it)

print(a, b, c, d)
0 0 1 2

Цикл for использует итераторы для прохода по итерируемым объектам и сам вызывает функцию next до тех пор, пока не встретит исключение StopIteration.

Среди итерируемых объектов, мы уже прошли

  • все последовательности: списки, кортежи, строки и т.п.

  • range;

  • словари и все связанные с ними view (dict_keys, dict_values и dict_items);

а так же

  • массивы NumPy;

  • таблица и столбцы pandas;

  • и т.п.

Нестандартные итераторы

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

reversed

Встроенная функция reversed создаёт итератор, который пробегает по коллекции в обратном порядке относительно стандартного итератора iter.

x = [1, 2, 3]
x_reversed = reversed(x)
print(x_reversed)
print(list(x_reversed))
<list_reverseiterator object at 0x00000217B7BA8580>
[3, 2, 1]

enumerate

Встроенная функция enumerate возвращает “нумерующий итератор”, т.е. итератор, который помимо следующего элемента контейнера возвращает ещё и его номер.

L = list("abc")
L_enumerated = enumerate(L)
print(L)
print(L_enumerated)
print(list(L_enumerated))
['a', 'b', 'c']
<enumerate object at 0x00000217B79E1A00>
[(0, 'a'), (1, 'b'), (2, 'c')]

Часто используется в циклах, если нужен доступ к элементам контейнера по индексу. При этом, следующая запись

for i, _ in enumerate(iterable):
    ...

считается предпочтительнее, чем

for i in range(len(iterable)):
    ...
for i, x in enumerate(L):
    print(i, x)
0 a
1 b
2 c

Конечно, эти итераторы можно комбинировать.

for i, x in enumerate(reversed(L)):
    print(i, x)
0 c
1 b
2 a

zip

Встроенная функция zip создаёт итератор по нескольким итерируемым объектам, т.е. позволяет пробежаться по нескольким контейнерам одновременно.

x_list = [1, 2, 3]
y_list = list("abc")

for x, y in zip(x_list, y_list):
    print(x, y)
1 a
2 b
3 c

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

m = [[1, 2], [3, 4]]

for row in m: 
    print(row)

print()

for column in zip(*m):
    print(column)
[1, 2]
[3, 4]

(1, 3)
(2, 4)

Если коллекции внутри zip содержат разное количество элементов, то итерация прекратится, как только закончится элементы в самом коротком из них, а функция zip_longest из модуля стандартной библиотеки itertools позволяет итерировать до упора, пока хотя бы в одной последовательности остались элементы.

Модуль itertools

В модуле itertools определен ещё ряд полезных итераторов. Рассмотрим несколько из них.

zip_longest

Если коллекции внутри zip содержат разное количество элементов, то итерация прекратится, как только закончится элементы в самом коротком из них, а функция zip_longest из модуля itertools позволяет итерировать до упора, пока хотя бы в одной последовательности остались элементы.

from string import ascii_letters, digits
from itertools import zip_longest


for x, y in zip(digits, ascii_letters):
    print(f"{x}->{y}, ", end="")

print()

for x, y in zip_longest(digits, ascii_letters, fillvalue=float("NaN")):
    print(f"{x}->{y}, ", end="")
0->a, 1->b, 2->c, 3->d, 4->e, 5->f, 6->g, 7->h, 8->i, 9->j, 
0->a, 1->b, 2->c, 3->d, 4->e, 5->f, 6->g, 7->h, 8->i, 9->j, nan->k, nan->l, nan->m, nan->n, nan->o, nan->p, nan->q, nan->r, nan->s, nan->t, nan->u, nan->v, nan->w, nan->x, nan->y, nan->z, nan->A, nan->B, nan->C, nan->D, nan->E, nan->F, nan->G, nan->H, nan->I, nan->J, nan->K, nan->L, nan->M, nan->N, nan->O, nan->P, nan->Q, nan->R, nan->S, nan->T, nan->U, nan->V, nan->W, nan->X, nan->Y, nan->Z, 

product

Итератор product позволяет итерировать по декартову (или внешнему) произведению других итерируемых объектов. В некоторых ситуациях позволяет упростить конструкции из вложенных циклов.

from itertools import product

X = [0, 1, 2]
Y = "abc"

print([(x, y) for x in X for y in Y])
print(list(product(X, Y)))
[(0, 'a'), (0, 'b'), (0, 'c'), (1, 'a'), (1, 'b'), (1, 'c'), (2, 'a'), (2, 'b'), (2, 'c')]
[(0, 'a'), (0, 'b'), (0, 'c'), (1, 'a'), (1, 'b'), (1, 'c'), (2, 'a'), (2, 'b'), (2, 'c')]

permutations

Итератор permutations пробегается по списку всех возможных перестановок итерируемого аргумента.

from itertools import permutations

print(list(permutations("abc")))
[('a', 'b', 'c'), ('a', 'c', 'b'), ('b', 'a', 'c'), ('b', 'c', 'a'), ('c', 'a', 'b'), ('c', 'b', 'a')]

Note

Сгенерировать случайную перестановку можно методом shuffle или методом sample при k равном длине последовательности. shuffle случайным образом тасует элементы на месте, а sample возвращает новый список.

combinations и combinations_with_replacements

Метод combinations возвращает все возможные способы выбрать r элементов итерируемого объекта без повторов.

from itertools import combinations

for x, y in combinations(range(3), 2):
    print(f"{x} and {y}") 
0 and 1
0 and 2
1 and 2

Метод combinations_with_replacements делает то же самое, но допускает повторы.

from itertools import combinations_with_replacement

for x, y in combinations_with_replacement(range(3), 2):
    print(f"{x} and {y}") 
0 and 0
0 and 1
0 and 2
1 and 1
1 and 2
2 and 2