Конспект посвящён устройству итераторов и генераторов в Python
Итераторы
Определения
Если формально, то итераторами называются объекты, в которых реализованы следующие методы:
__iter__(), возвращающий сам итератор;
__next__(), возвращающий следующий элемент последовательности. Если элементов больше нет, возбуждается исключение StopIteration.
Проще говоря, итератор — объект, элементы которого можно получить единожды.
Итерируемыми называются объекты, по которым можно создать итератор (в которых есть метод __iter__()), поэтому таковыми в Python являются сами итераторы и все стандартные коллекции.
Коллекция — итерируемый объект, который содержит элементы, позволяет к ним обращаться и имеет соответствующие для себя операции.
Последовательность — коллекция, элементы которой проиндексированы.
Создание и работа с итераторами
Чтобы создать итератор на основе некоторой коллекции, нужно обернуть её в функцию iter(). Чтобы получить следующий элемент коллекции, требуется вызвать функцию next(iterator, default) и положить в неё итератор.
Инфо
Сами по себе коллекции не являются итераторами, но на их основе можно создать сколько угодно итераторов
По итератору можно пройтись лишь единожды! При этом возможно только последовательно запрашивать элементы, но не обращаться по индексу
Цикл for также работает с итераторами. Когда в него передаётся итерируемый объект, он создаёт итератор на его основе и запрашивает элементы вплоть до получения исключения StopIteration. Благодаря этому, в for можно передать и список, и кортеж, и строку, и объект типа range, и многие другие объекты, которые имеют свои итераторы.
Оператор принадлежности in работает также и с итераторами, путём перебора всех элементов. Из этого вытекает, что итератор “опустошается” из-за работы in. Например:
Итераторы работают лениво, то есть возвращают по одному объекту по запросу. Некоторые встроенные функции, такие как map(), filter(), zip(), enumerate(), reversed() и т.д. возвращают ленивые итераторы, но, например, когда итератор создаётся на основе коллекции, ленивые вычисления не работают, поскольку в памяти уже есть все элементы.
Инфо
Объекты, возвращаемые функцией range(), в отличие от объектов, возвращаемых функциями enumerate(), zip(), reversed(), итераторами не являются
1
2
3
4
5
6
7
8
sentence='In the face of ambiguity refuse the temptation to guess'filter_iterator=filter(lambdaword:len(word)>4,sentence.split())map_iterator=map(lambdaword:word.upper(),filter_iterator)enumerate_iterator=enumerate(map_iterator,1)forindex,valueinenumerate_iterator:print(f'{index}. {value}')
Все три объекта filter_iterator, map_iterator, enumerate_iterator являются итераторами. Они не хранят все данные в памяти, а создают и выдают их по мере того, как их запрашивают. Другими словами, при обращении к очередному элементу enumerate_iterator произойдет последовательное обращение сначала к элементу map_iterator, а затем к элементу filter_iterator.
Особенность iter()
Функция iter() принимает ещё один аргумент. Вот как выглядит её сигнатура: iter(iterable, sentinel), где sentinel — стоп-значение, дойдя до которого итератор вызывает исключение StopIteration.
Благодаря этому добавляются сценарии использования функции, например, создать бесконечный итератор, генерирующий 0:
Генератор — функция или выражение, возвращающее итератор (объект типа generator), который создаёт значения по мере запроса через yield. Когда возвращать больше нечего, генератор вызывает исключение StopIteration.
Инфо
Генератор является подтипом итератора, поэтому наследует все его особенности
В отличии от обычных функций, генераторы используют ключевое слово yield вместо return для возврата значений. Также генераторы сохраняют свои локальные переменные от вызова к вызову.
Рассмотрим функцию-генератор, создающую последовательность целых чисел от 0 до n-1:
for получает итератор: iterator = iter(generate_AB()) и на каждой итерации вызывает функцию next(iterator).
Во время первой итерации и первом вызове next(iterator) генератор, перед тем как сгенерировать значение 'A' (то есть дойти до строки yield 'A'), сначала выполняет строку print('start').
Во время второй итерации генератор, перед тем как сгенерировать значение 'B', сначала выполняет строку print('continue').
Во время третьей итерации генератор выполняет строку print('end') и завершает свою работу, возбуждая исключение StopIteration. Цикл for перехватывает это исключение и нормально завершается.
Инфо
Слово return внутри генератора будет приводить к возбуждению исключения StopIteration без возвращения значения
Примеры использования генераторов
Пример 1
Генератор counter(), порождающий последовательность целых чисел от значения low до high включительно с шагом один:
Генератор factorials(), порождающий бесконечную последовательность факториалов всех натуральных чисел:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
deffactorials():value=1index=1whileTrue:yieldvalueindex+=1value*=indexinfinite_factorials=factorials()forindex,numinenumerate(infinite_factorials,1):ifindex<=10:print(f'Факториал числа {index} равен {num}')
1
2
3
4
5
6
7
8
9
10
Факториал числа 1 равен 1
Факториал числа 2 равен 2
Факториал числа 3 равен 6
Факториал числа 4 равен 24
Факториал числа 5 равен 120
Факториал числа 6 равен 720
Факториал числа 7 равен 5040
Факториал числа 8 равен 40320
Факториал числа 9 равен 362880
Факториал числа 10 равен 3628800
yield from
yield from — конструкция, делегирующая генерацию значений другому генератору или итерируемому объекту. По-сути она заменяет цикл for внутри генератора и позволяет встроить один генератор внутрь другого.
Также yield from можно использовать и с другими итерируемыми объектами:
1
2
3
4
5
6
7
8
9
10
defget_data():yield fromrange(5)yield from'ABC'foriinget_data():print(i,end=' ')# Вывод: 0 1 2 3 4 A B C
Более того, конструкции yield и yield from можно использовать для написания рекурсивных генераторов. Следующий пример определяет бесконечный генератор, который порождает все целые числа со значения start.
1
2
3
4
5
6
7
8
9
10
11
12
13
defnumbers(start):ifnotisinstance(start,int):raiseTypeError('Argument must be an integer')yieldstartyield fromnumbers(start+1)forindex,numberinenumerate(numbers(3)):ifindex>5:breakprint(number,' ')# Вывод: 3 4 5 6 7 8
Генераторные выражения
Генераторное выражение — компактная запись генератора, похожая на list comprehension, но ограниченная круглыми скобками. Его значения не хранятся в памяти, а вычисляются по мере вызова next(), как и в обычном генераторе.
squares=(i**2foriinrange(1,7))# Создаем генератор с помощью генераторного выраженияcapitals=(s.upper()forsin'abc')# Создаем генератор с помощью генераторного выраженияstars=('*'foriinrange(5))# Создаем генератор с помощью генераторного выраженияfornuminsquares:print(num)print(next(capitals))print(*stars,end=' ')
1
2
3
4
5
6
7
8
9
# Вывод:
1
4
9
16
25
36
A
* * * * *
Инфо
Генераторное выражение нельзя писать без скобок, но если оно передаётся в качестве единственного аргумента в функцию, скобки можно опустить
Конвейеры генераторов
Конвейер генераторов — цепочка генераторных выражений или функций, в которой каждый генератор обрабатывает данные по мере поступления от предыдущего, без загрузки всех данных в память.
Конвейеры данных, построенные на генераторах позволяют скомпоновать код для обработки больших наборов данных без использования большого количества памяти.