Итераторы и выражения-генераторы

На этом занятии мы с вами поговорим об итераторах и выражениях-генераторах. С генераторами списков мы уже сталкивались, например, когда создавали список вот таким образом:

a = [x**2 for x in range(10)]

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

b = (x**2 for x in range(10))

Если мы теперь отобразим переменную b, то увидим:

<generator object <genexpr> at 0x0000020E8F429C80>

что эта переменная ссылается на объект-генератор. Вообще,

Генератор – это итератор, элементы которого можно перебирать (итерировать) только один раз.

И в этом определении мы сталкиваемся с термином итератор:

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

Наконец, последний термин, что нам пригодится, звучит так:

Итерируемый объект – это объект, который позволяет поочередно обойти свои элементы и может быть преобразован к итератору.

Теперь, давайте рассмотрим все на конкретных примерах. Самый распространенный итерируемый объект в Python – это список:

a = [1,2,3]

но мы не можем его обойти с помощью итератора, используя функцию next:

next(a)

потому что список – это не итератор. Но мы любой итерируемый объект можем легко преобразовать в итератор с помощью функции iter:

iter(a)

На выходе образуется объект-итератор для списка. Сохраним его в переменной:

it = iter(a)

Теперь, элементы списка можно обойти с помощью этого итератора:

next(it)

При первом ее вызове она возвратит первое значение списка a и изменит позицию итератора it, переместив его на следующий элемент. Поэтому при втором вызове:

next(it)

мы получим уже значение второго элемента и так до конца списка:

next(it)

Если вызвать функцию next когда мы уже дошли до конца списка, то она возвратит ошибку:

next(it)

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

Теперь, возвращаясь к выражению-генератору:

b = (x**2 for x in range(10))

переменную b можно воспринимать как итератор и перебирать список через функцию next:

next(b)

возвратит первое значение. Повторный вызов:

next(b)

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

Итераторы очень удобно использовать в цикле for:

b = (x**2 for x in range(10))
for i in b:
   print(i, end=" ")

Здесь нам не нужно использовать функцию next для перехода к следующему значению. Это автоматически выполняет оператор in в for. Но использовать его можно только один раз. Если мы выполним цикл с этим же итератором еще раз:

print("\nnew loop")
for i in b:
    print(i, end=" ")

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

Некоторые функции, такие как:

sum, max, min

позволяют работать непосредственно с итераторами. То есть, можно выполнять вот такие операции:

b = (x**2 for x in range(10))
sum(b)

Будет вычислена сумма квадратов соответствующих значений. И здесь возникает, наверное, давно назревший вопрос: зачем вообще нужны эти выражения-генераторы? У этих объектов есть одно существенное преимущество по сравнению с обычными списками: они не хранят в памяти все значения сразу, а генерируют их по мере необходимости, то есть, при проходе к следующему значению. Например, если возникает необходимость оперировать очень большим списком:

lst = list(range(1000000000000))

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

lst = (x for x in range(1000000000))

например, в цикле for:

lst = (x for x in range(1000000000))
for i in lst:
    print(i, end=" ")
    if i > 100: break

И работать все это будет достаточно быстро, так как lst не хранит в памяти элементы, а вычисляет их налету в цикле for. Правда, из-за этого нельзя определить число элементов в генераторе при помощи функции len:

a = (x for x in range(10, 20))
len(a)

или получить доступ к его отдельному элементу по индексу:

a[2]

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

a = (x for x in range(10, 20))
b = list(a)

В результате, переменная b ссылается на список:

[10, 11, 12, 13, 14, 15, 16, 17, 18, 19]

с которым мы уже можем работать как со списком.

Видео по теме