Итераторы и генераторы

borntohack

змееуст
Команда форума
Модератор
Апр 22, 2020
73
49
18
35
Москва, РФ
Практически во всех статьях по python мы сталкиваемся с понятиями итеративности - т.е. такому свойству объекта, которое позволяет перечислять его элементы.
Стоит отметить, что далеко не все объекты python итеративны. Например, такие объекты как "строка", "список", "кортеж", "словарь" и многие другие - итеративны, а объект "число" - нет.
Так о чем речь?

Давайте определимся с терминами:
ИТЕРАЦИЯ - часть процесса последовательной обработки некоторого набора данных. Своеобразный "этап" обработки, или "очередной проход" цикла.
ИТЕРИРУЕМОЕ (Итерируемый объект, итеративный объект, ITERABLE) - некоторый объект, содержащий набор данных, по которому можно итерироваться - т.е. осуществлять итерации. Обычно, такой объект характеризуется наличием свойств __iter__ или __getitem__
ИТЕРАТОР - элемент набора данных итерируемого. То, что получено в результате итерации. Обычно такой объект имеет свойство __next__ для указания на следующую итерацию.

Давайте разбираться "на пальцах" на примере цикла for проходящего по списку значений:
Python:
lst = [1,2,3,4,5,6,7,8,9]
for i in lst:
    print(i)
Выполнение этого кода выведет 9 строк со значениями от 1 до 9. Что здесь чем является?
1 строка: lst = [1,2,3,4,5,6,7,8,9] - создает объект список со значениями 1-9.
2 строка начинает цикл for по итерируемому - списку. При этом в качестве итератора создается перменная i. На каждой итерации эта переменная будет содержать значение итератора - т.е. элемента списка.
3 строка выводит содержимое итератора на экран.

Поменяем код так:
Python:
lst = "123456789"
for i in lst:
    print(i)
и.... ничего не поменялось.
Это происходит потому, что хоть мы и заменили итерируемое со списка на строку, итеративность обоих этих элементов (в рамках указанных условий) одинакова. Оба этих итерируемых возвращают в качестве итератора значение от одного до 9. Но, разница все же есть.
Итераторы строки - это ее символы. Итераторы списка - это элементы
Если добавить в наши эксперименты двузначное число (10) то поведение начнет отличаться:
Python:
lst = [1,2,3,4,5,6,7,8,9,10]
s = "12345678910"
for i in lst:
    print(i)
for i in s:
    print(i)
В первом случае мы ожидаемо получим 10 строк со значением от 1 до 10, а во втором - строк будет уже 11. Последние две будут иметь значение "1" и "0" поскольку в строке 11 символов.

Давайте попробуем проитерироваться по числу:
Python:
for i in 100:
    print(i)
Получим ошибку TypeError: 'int' object is not iterable - дословно гласящую, что объект типа int не является итерируемым.
Т.е. вопреки ожиданиям получить 1,0,0 мы получаем ошибку, поскольку итерироваться по числам нельзя. Кроме чисел есть ряд других неитерируемых объектов. Будьте внимательны при работе с циклами и итерациями.

Теперь рассмотрим понятие Генератора.
Это итерируемый объект, который не резервирует под себя область памяти, а генерирует свои итераторы на лету. Это делает его высокоэффективным в плане использования ресурсов, но позволяет итерироваться по нему лишь один раз.
Python имеет некоторые встроенные генераторы, например range, но так же позволяет писать и свои генераторы.
В общем случае генераторы описываются тем же синтаксисом, что и включения (comprehensions), но всё же это разные типы объектов:
Python:
lst = [1,2,3,4,5,6,7,8,9,10]
gen = (i for i in lst if i%2) #например, мы хотим генерировать только нечетные числа из списка lst
for n in gen:
    print(n)
В отличии от спискового включения генератор не занимает память всеми своими значениями. Вместо этого они вычисляются в каждой итерации и возвращаются из генератора в качестве итератора. Таким образом экономится значительное количество ресурсов памяти (если работать с большими объемами данных, например массивными таблицами excel на сотни тысяч строк и т.д.)

Так же генератор может быть определен в виде функции, оператор возврата которой вместо return определен как yield:
Python:
def some_generator(lst):
    for i in lst:
        if i%2:
            yield i

lst = [1,2,3,4,5,6,7,8,9,10]
for i in some_generator(lst):
    print(i)
Оператор yield не прекращает выполнение функции, хотя и возвращает вычисленное значение. Поскольку итератор на каждой итерации перезаписывается, занятая таким генератором область памяти эквивалентна области, занимаемой одним итератором

Естественно, у меня в запасе есть то, на что нужно обратить внимание) Если вы передаете генератор, определяемый в виде включения, в функцию в качестве аргумента - то писать вторые скобки не нужно:
например для суммирования генератора функцией sum
Python:
s = sum(i for i in range(100) if i%2)
Пользуйтесь, учитесь, понимайте, и конечно, задавайте вопросы!
 
Последнее редактирование:

vs2007

Пользователь
Пользователь
Май 24, 2020
16
5
3
Очень понятные описания. А почему бы не продолжить в сторону конструкций вида x=yield <Выражение>, yield from ... (а там и до await недалеко).
 

Форум IT Специалистов