Итерации и генераторы в Python
- Протокол итераций в Python
- Выражения-генераторы и генерация последовательностей
- Функции-генераторы и ключевое слово yield
- Краткие итоги параграфа
- Вопросы и задания для самоконтроля
Протокол итераций в Python
В ходе решения широкого круга задач довольно часто возникает необходимость в циклическом или последовательном переборе элементов различных последовательностей, атрибутов объектов, строк файлов и т.д. Для этих целей в Python используется так называемый протокол итераций, который позволяет с легкостью обрабатывать требуемое количество элементов любого итерируемого объекта. Именно этот протокол лежит в основе работы рассмотренного нами цикла for, различных генераторов и некоторых встроенных функций типа map или filter. Однако прежде, чем перейти к рассмотрению самого протокола, давайте уточним некоторые использовавшиеся ранее понятия.
Итерация (от англ. iteration) – это процесс обработки данных, при котором действия повторяются многократно, не приводя при этом к вызовам самих себя. Одно повторение (один виток) такого процесса мы также будем называть итерацией.
Пусть, например, нам нужно организовать цикл for по элементам списка из пяти элементов. Тогда все элементы списка цикл пройдет за пять итераций (т.е. повторений или витков).
Итерируемый объект (от англ. iterable object) – это любой объект, набор элементов которого можно обрабатывать многократно и по-одному за раз. В Python все такие объекты реализуют метод __iter__() (возвращает итератор объекта) и/или __getitem__(index) (позволяет получать элементы объекта по индексу).
По сути итерируемыми в Python являются все типы рассмотренных нами встроенных последовательностей (см. пример №1), а также любые объекты, элементы которых можно обойти с помощью имеющихся в языке инструментов совершения итераций, например, уже известного нам цикла for.
# Итерируемые объекты имеют методы
# __iter__ или __getitem__
# Проверяем строки на наличие методов.
s = 'abc'
# Получим True.
print('s.__iter__:', hasattr(s, '__iter__'))
# Получим True.
print('s.__getitem__:', hasattr(s, '__getitem__'))
# Запускаем цикл для обхода символов строки.
for simb in s: print(simb, end=' ')
print(end='\n\n')
# Проверяем списки на __iter__ и __getitem__.
li = [1, 2, 3]
# Получим True.
print('li.__iter__:', hasattr(li, '__iter__'))
# Получим True.
print('li.__getitem__:', hasattr(li, '__getitem__'))
# Запускаем цикл для обхода эл-тов списка.
for elem in li: print(elem, end=' ')
print(end='\n\n')
# Проверяем словари на __iter__ и __getitem__ .
d = {'a': 1, 'b': 2, 'c': 3}
# Получим True.
print('d.__iter__:', hasattr(d, '__iter__'))
# Получим True.
print('d.__getitem__:', hasattr(d, '__getitem__'))
# Запускаем цикл для обхода ключей словаря.
for key in d: print(key, end=' ')
print(end='\n\n')
# Проверяем мн-ва на __iter__ и __getitem__ .
st = {1, 2, 3}
# Получим True.
print('st.__iter__:', hasattr(st, '__iter__'))
# Получим False.
print('st.__getitem__:', hasattr(st, '__getitem__'))
# Запускаем цикл для обхода эл-тов множества.
for elem in st: print(elem, end=' ')
print(end='\n\n')
# Проверяем числа на __iter__ и __getitem__ .
f = 1.23
# Получим False.
print('f.__iter__:', hasattr(f, '__iter__'))
# Получим False.
print('f.__getitem__:', hasattr(f, '__getitem__'))
# Пытаемся запустить цикл для обхода цифр числа, но
# получаем «'float' object is not iterable».
for n in f: print(n, end=' ')
s.__iter__: True
s.__getitem__: True
a b c
li.__iter__: True
li.__getitem__: True
1 2 3
d.__iter__: True
d.__getitem__: True
a b c
st.__iter__: True
st.__getitem__: False
1 2 3
f.__iter__: False
f.__getitem__: False
'float' object is not iterable
Пример №1. Примеры встроенных итерируемых и неитерируемых объектов.
В нашем примере мы с помощью встроенной функции hasattr(object, name) проверили передаваемые ей объекты встроенных типов на наличие методов __iter__ и __getitem__, после чего попробовали организовать сканирование объектов циклом for. Сделать это у нас получилось для строк, списков, словарей и множеств. Правда у множеств не оказалось метода __getitem__(index) (у элементов множеств просто нет индексов), но поскольку для итерационных инструментов достаточно наличия хотя бы одного из указанных методов, цикл запустился без каких-либо проблем. А вот для чисел необходимые методы обнаружены не были, т.к. в Python числа относятся к неизменяемым и неитерируемым объектам. В результате вместо предполагаемого цикла по цифрам вещественного числа мы получили ошибку.
Здесь стоит добавить, что в Python имеется весьма удобная встроенная функция iter(object), которая проверяет переданный ей объект на наличие методов __iter__ и __getitem__ и в случае наличия хотя бы одного из них возвращает готовый итератор объекта. Если объект окажется неитерируемым, функция возбудит исключение TypeError. Кстати, мы уже несколько раз использовали понятие итератора, но так и не дали ему определение. Давайте исправим этот момент.
Итератор (от англ. iterator) – это специальный объект, в котором реализован метод __next__(), позволяющий при каждом новом вызове получать следующий элемент итерируемого объекта. Итераторы возвращаются, например, методом __iter__(), встроенной функцией iter(object), выражениями-генераторами и другими инструментами для их получения.
Из всего вышесказанного следует, что итерируемый объект предоставляет нам некоторый набор элементов, а также инструмент для многократной поэлементной обработки этого набора, т.е. итератор для организации итераций. Например, у строк таким набором являются символы, у списков и множеств – их элементы, у словарей – ключи и значения, у объектов – атрибуты, у файлов – строки и т.д. При этом каким бы доступным итерационным инструментом мы не воспользовались, интерпретатор совершит итерации не хаотично, а вполне упорядоченно, согласно принятому протоколу итераций, который заключается в следующем:
- у переданного итерируемого объекта вызывается метод __iter__, который возвращает итератор объекта, хранящий в себе информацию о всех элементах итерируемого объекта и способный выдавать их по одному за раз;
- затем для получения каждого элемента итерируемого объекта у полученного итератора начинает вызываться метод __next__();
- каждый последующий вызов метода __next__() возвращает следующий элемент итерируемого объекта, что продолжается до тех пор, пока очередной вызов метода не возбудит исключение StopIteration;
- возбуждение исключения StopIteration приводит к остановке итераций и выходу из процесса.
Опять же, в Python имеется удобный аналог метода __next__ в виде встроенной функции next(iterator[, default]), которая при отсутствии дополнительного аргумента просто вызывает метод __next__ итератора. Если же дополнительный аргумент присутствует, то по окончании итераций, функция перехватывает исключение StopIteration и вместо него возвращает переданный аргумент default (см. пример №2).
Конечно, когда мы запускаем, например, цикл for, сама работа протокола итераций от нас скрывается и совершается в автоматическом режиме на заднем плане, предоставляя нам уже готовые результаты. Это делается специально, чтобы использование итерационных инструментов для пользователей было максимально простым и удобным. Однако при необходимости никто не запрещает совершать итерации в ручном режиме, используя для этого протокол итераций непосредственно (опять же см. пример №2).
# Создаем список.
li = [1, 2, 3]
# Выводим эл-ты циклом в авторежиме.
for e in li: print(e, end=' ')
print()
# Используем протокол итераций вручную.
# Получаем итератор списка посредством функции.
li_iter = iter(li)
# Получаем эл-ты с помощью функции next()
print(next(li_iter), end=' ')
print(next(li_iter), end=' ')
print(next(li_iter), end=' ')
# Вместо StopIteration выведет 'Happy end!'.
print(next(li_iter, 'Happy end!'))
1 2 3
1 2 3 Happy end!
Пример №2. Сравнение итераций в автоматическом и ручном режиме.
Согласитесь, использовать протокол итераций вручную без острой необходимости не совсем рационально. Именно поэтому вся черновая и рутинная работа выполняется интерпретатором, позволяя программисту экономить время в процессе написания кода. Более того, использование в итерациях вместо самих итерируемых объектов их итераторов позволяет экономить еще и значительные объемы памяти. И чем больше элементов содержит итерируемый объект, тем большей будет такая экономия (см. пример №3).
# Импортируем модуль sys.
import sys
# Это небольшой список.
li_1 = [1, 2, 3]
# Получаем итератор этого списка.
i_1 = li_1.__iter__()
# Выводим размеры списка и его итератора.
# 120 байт.
print('Размер li_1:', sys.getsizeof(li_1))
# 48 байт.
print('Размер его итератора:', sys.getsizeof(i_1), '\n')
# Возьмем список побольше.
li_2 = list(range(0, 1000))
# Получаем итератор списка при помощи встроенной iter().
i_2 = iter(li_2)
# Выводим размеры списка и его итератора.
# 8056 байт.
print('Размер li_2:', sys.getsizeof(li_2))
# 48 байт.
print('Размер его итератора:', sys.getsizeof(i_2))
Размер li_1: 120
Размер его итератора: 48
Размер li_2: 8056
Размер его итератора: 48
Пример №3. Экономия памяти итераторами.
Как видим, даже небольшой список по размеру превышает размер итератора. Но, что важно, при увеличении количества элементов любого итерируемого объекта его размер будет стремительно расти, а вот размер итератора останется прежним. Поэтому, например, при построчном чтении файла при помощи цикла for лучше использовать его итератор, а не имеющийся у файлов метод readlines(), который загружает на сканирование весь файл целиком. Тем более, что при огромных размерах файла он может вообще не поместиться в память компьютера.
Также стоит отметить, что файловые объекты и некоторые другие типы имеют собственные итераторы, а значит и собственный метод __next__. Поэтому для работы с файлами не требуется получать другой объект, т.е. итератор. А вот списки и многие другие встроенные объекты не имеют собственных итераторов, т.к. они поддерживают возможность участия сразу в нескольких итерациях. Поэтому для начала итерации по таким объектам нужно предварительно вызывать функцию iter (см. пример №4).
# Пусть у нас есть файл lang.txt с 2-мя строчками:
# Язык: Pyton
# Версия: 3.13
# Получаем файловый объект.
f = open('lang.txt')
# Его можно читать напрямую (итератор уже встроен).
# Выведет строку «Язык: Pyton».
print(f.__next__())
# Выведет строку «Версия: 3.13».
print(f.__next__(), end='\n\n')
# У списков встроенных итераторов нет,
li = [1, 2]
# поэтому итераторы получаем через метод iter.
i_1 = li.__iter__()
i_2 = li.__iter__()
# Зато они могут участвовать сразу в нескольких итерациях.
print('1-й виток 1-ой итерации:', next(i_1, 'End!'))
print('1-й виток 2-ой итерации', next(i_2, 'End!'))
print('2-й виток 1-ой итерации:', next(i_1, 'End!'))
print('2-й виток 2-ой итерации', next(i_2, 'End!'))
print('1-я итерация:', next(i_1, 'End!'))
print('2-я итерация:', next(i_2, 'End!'))
print('1-я итерация:', next(i_1, 'End!'))
print('2-я итерация:', next(i_2, 'End!'))
Язык: Pyton
Версия: 3.13
1-й виток 1-ой итерации: 1
1-й виток 2-ой итерации 1
2-й виток 1-ой итерации: 2
2-й виток 2-ой итерации 2
1-я итерация: End!
2-я итерация: End!
1-я итерация: End!
2-я итерация: End!
Пример №4. Итерации по объектам со встроенными итераторами и без них.
Обратите внимание, что после того, как итерации по итераторам были завершены, повторное их использование стало невозможным. Это связано с тем, что итераторы в языке Python представляют собой одноразовые инструменты. При каждом новом вызове метода __next__() из итератора извлекается очередной элемент, в результате чего итератор постепенно опустошается и в конечном итоге метод __next__ начинает постоянно возбуждать исключение StopIteration. Поэтому пустые итераторы становятся непригодными для дальнейшего использования, а повторный запуск итераций по тому же итерируемому объекту может быть возобновлен только после создания нового итератора.
Выражения-генераторы и генерация последовательностей в Python
Ранее мы уже использовали различные генераторы для создания списков, словарей и множеств. Но дело в том, что все эти генераторы по сути могут быть получены из более общих первичных конструкций, называемых выражениями-генераторами. Чтобы понять почему это так, давайте рассмотрим новое понятие более подробно.
Выражение-генератор (от англ. generator expression) – это конструкция на базе цикла for, которая применяет заданное выражение к каждому элементу переданного итерируемого объекта в соответствии с заданным условием и затем возвращает объект-генератор со встроенным итератором, содержащий информацию о всех сформированных выражением элементах.
В общем случае синтаксис генератора-выражения в Python можно представить в следующем виде:
gen_obj = (expression for variable in iterable_obj [if condition]), где
- gen_obj – объект-генератор со встроенным итератором, возвращаемый генератором-выражением;
- expression – выражение, формирующее новые элементы для итератора;
- variable – переменная-счетчик цикла for, которой поочередно передаются элементы итерируемого объекта iterable_obj;
- iterable_obj – итерируемый объект, который служит источником элементов для создаваемого генератором итератора;
- condition – необязательное условное выражение, в соответствии с которым происходит отбор элементов итерируемого объекта iterable_obj.
В общей форме синтаксис генератора-выражения может показаться в некоторой мере затруднительным для восприятия, однако на практике все выглядит значительно проще (см. пример №5).
# Выражение-генератор без условия.
gen_1 = (x**2 for x in range(2, 5))
# <generator object main.<locals>.<genexpr> at 0x000002861726A9D0>
print(gen_1)
# Является ли объект-генератор итератором?
print(gen_1.__next__(), end=' ')
print(gen_1.__next__(), end=' ')
print(gen_1.__next__(), end=' ')
print(next(gen_1, 'StopIteration'))
# Выражение-генератор с условием if.
gen_2 = (x**2 for x in range(2, 10) if x > 4)
# Выведем значения автоматически циклом for.
for elem in gen_2: print(elem, end=' ')
<generator object main.<locals>.<genexpr> at 0x0000019F4606A960>
4 9 16 StopIteration
25 36 49 64 81
Пример №5. Создание выражений-генераторов.
Как видим, процесс создания выражений-генераторов практически ничем не отличается от создания генераторов списков или генераторов множеств. Разница заключается лишь в том, что вместо круглых скобок мы используем соответственно квадратные или фигурные скобки. Однако похожим является только синтаксис конструкций, результаты у них получаются совершенно разные, т.к. генераторы списков и множеств возвращают не итераторы, а уже готовые списки и множества, заполненные сгенерированными элементами. Но благодаря тому, что выражения-генераторы создают итераторы вместо полного списка или множества с результатами, они позволяют оптимизировать использование памяти в ходе работы программы. Это и является их первостепенным предназначением. Однако на практике выражения-генераторы могут работать несколько медленнее, поэтому их лучше использовать именно тогда, когда объем результатов достаточно велик (см. пример №6).
# Импортируем модуль sys.
import sys
# Генератор-выражение возвращает итератор.
gen_1 = (x for x in range(0, 1000))
# Получим 104 байта.
print('Размер gen_1:', sys.getsizeof(gen_1))
# Генератор списка возвращает список.
li_1 = [x for x in range(0, 1000)]
# Получим 8856 байт.
print('Размер li_1:', sys.getsizeof(li_1))
# Генератор мн-ва возвращает множество.
s_1 = {x for x in range(0, 1000)}
# Получим 32984 байта.
print('Размер s_1:', sys.getsizeof(s_1))
Размер gen_1: 104
Размер li_1: 8856
Размер s_1: 32984
Пример №6. Экономия памяти выражениями-генераторами.
Теперь давайте разберемся, как генераторы списков и множеств могут быть получены из выражений-генераторов. Думается, что ответ практически очевиден, нужно просто использовать в отношении сгенерированного итератора вызов конструктора list() или set() для принудительного получения сразу всего списка или же множества с результатами (см. пример №7).
# Приводим к списку при помощи вызова list().
li_1 = list((x for x in range(3)))
# Выведет li_1: [0, 1, 2].
print('li_1:', li_1)
# Приводим к мн-ву при помощи вызова set().
s_1 = set((x for x in range(3)))
# Вывело s_1: {0, 1, 2}.
print('s_1:', s_1)
li_1: [0, 1, 2]
s_1: {0, 1, 2}
Пример №7. Получение списков и множеств из генераторов-выражений.
Как видим, такой способ действует и по функциональности в принципе является эквивалентом использования квадратных и фигурных скобок. Но, опять же, использование последних выглядит проще, особенно в длинных выражениях.
Хорошо, а что можно сказать про генераторы словарей в контексте генераторов-выражений? Да практически все тоже самое, что и про генераторы списков и множеств. Вот только конструкция самого генератора словаря выглядит чуть сложнее (см. пример №8).
# Это наш источник ключей и значений для словаря.
li = [('one', 1), ('two', 2), ('three', 3)]
# Простой генератор словаря.
d_1 = {k: v for k, v in li}
# Выведет d_1 -> {'one': 1, 'two': 2, 'three': 3}.
print('d_1 ->', d_1)
# Словарь из выражения-генератора.
d_2 = dict(((k, v) for k, v in li))
# Выведет d_2 -> {'one': 1, 'two': 2, 'three': 3}.
print('d_2 ->', d_2)
# Генератор словаря с условием.
d_3 = {k: v for k, v in li if v != 3}
# Выведет d_3 -> {'one': 1, 'two': 2}.
print('d_3 ->', d_3)
# Словарь со значением по умолчанию.
d_4 = {k: None for k in 'abc' if k == 'a'}
# Выведет d_4 -> {'a': None}.
print('d_4 ->', d_4)
d_1 -> {'one': 1, 'two': 2, 'three': 3}
d_2 -> {'one': 1, 'two': 2, 'three': 3}
d_3 -> {'one': 1, 'two': 2}
d_4 -> {'a': None}
Пример №8. Создание словарей с помощью генераторов.
Думается, что вы обратили внимание на второй случай, где мы в генераторе-выражении сохраняли в итератор не пары значений по отдельности, а кортежи с парами. Именно из них потом конструктор типа и сформировал нам готовый словарь. Опять же, в генераторах словарей все эти промежуточные операции делаются в скрытом режиме, что делает их использование для нас несколько проще.
Функции-генераторы и ключевое слово yield в Python
На данный момент мы познакомились с целым рядом генераторов: генераторами списков, словарей, множеств, а также генераторами-выражениями. Первые три вида генераторов выдают нам готовые наборы значений целиком, в то время как генераторы-выражения возвращают итераторы, позволяя экономить память и производить дополнительные вычисления между операциями получения результатов. Однако результаты могут возвращаться еще и функциями. А это могут быть все те же списки, словари или множества, которые не всегда нужны нам сразу целиком. Для таких случаев в Python имеется еще один вид генераторов – функции-генераторы, которые мы сейчас и рассмотрим.
Функция-генератор (от англ. generator function) – это специальная функция, которая использует инструкцию yield, способную приостанавливать выполнение функции с сохранением текущего состояния до следующего обращения к ней, и которая при вызове возвращает специальный объект-генератор со встроенным итератором, позволяющий по требованию возобновлять дальнейшее выполнение функции, начиная с момента последней приостановки процесса инструкцией yield.
В принципе функции-генераторы оформляются как обычные инструкции def, но с дополнительным включением инструкции yield, которая замораживает состояние функции и возвращает в точку вызова функции объект-генератор со встроенным итератором. А итераторы, как мы знаем, имеют в своем арсенале метод __next__. В нашем случае вызов этого метода приводит к снятию паузы и продолжению выполнения функции с места приостановки, т.е. с инструкции, которая следует за инструкцией yield (см. пример №9).
# Ф-ция возвращает список с n эл-ми.
def li_func(n):
# Список для заполнения.
li = []
# Запускаем цикл.
for i in range(1, n+1):
# Добавляем эл-т в список.
li.append(i)
# Возвращаем готовый список.
return li
# Выведет li_func(3) -> [1, 2, 3]
print('li_func(3) ->', li_func(3), end='\n\n')
# Ф-ция-генератор возвращает объект-генератор со встроенным итератором.
def gen_func(n):
# Запускаем цикл.
for i in range(1, n+1):
# Возвращаем значение и приостанавливаем
# выполнение ф-ции до следующего вызова.
yield i
# Выведет gen_func -> <function main...
print('gen_func ->', gen_func)
# Выведет gen_func(3) -> <generator object main...
print('gen_func(3) ->', gen_func(3), end='\n\n')
# Получаем два объекта-генератора (итераторы уже встроены).
gen_1 = gen_func(2)
gen_2 = gen_func(3)
# Запускаем итерации в ручном режиме.
print('Выводим 1-е значение в gen_1:', next(gen_1))
print('Выводим 1-е значение в gen_2:', next(gen_2))
print('Выводим 2-е значение в gen_1:', next(gen_1))
print('Выводим 2-е значение в gen_2:', next(gen_2))
print('1-й генератор пуст:', next(gen_1, 'End!'))
print('Выводим 3-е значение в gen_2:', next(gen_2))
print('1-й генератор пуст:', next(gen_1, 'End!'))
print('2-й генератор пуст:', next(gen_2, 'End!'))
li_func(3) -> [1, 2, 3]
gen_func -> <function main.<locals>.gen_func at 0x000001A792C48B80>
gen_func(3) -> <generator object main.<locals>.gen_func at 0x000001A7927DAB20>
Выводим 1-е значение в gen_1: 1
Выводим 1-е значение в gen_2: 1
Выводим 2-е значение в gen_1: 2
Выводим 2-е значение в gen_2: 2
1-й генератор пуст: End!
Выводим 3-е значение в gen_2: 3
1-й генератор пуст: End!
2-й генератор пуст: End!
Пример №9. Создание функций-генераторов.
Как видим, в отличие от обычных функций, которые возвращают значение и сразу же завершают работу, функции-генераторы лишь приостанавливают процесс, сохраняя информацию о его состоянии в возвращаемом при вызове объекте-генераторе со встроенным итератором, и возобновляют свое выполнение по требованию в момент вызова метода __next__. Это позволяет, опять же, функциям-генераторам экономить память и воспроизводить последовательности значений в течение длительного промежутка времени вместо того, чтобы создавать и возвращать всю последовательность значений за раз.
Также следует отметить, что при каждом отдельном вызове функции-генератора создается совершенно новый объект-генератор со своим встроенным итератором. Это дает возможность одновременного использования одной и той же функции-генератора из разных точек программы. Однако сколько бы итераторов мы не создавали, все они, как и выражения-генераторы, являются одноразовыми инструментами, постепенно опустошаясь при каждом новом вызове метода __next__ до тех пор, пока метод не начнет возбуждать исключение StopIteration.
Краткие итоги параграфа
- Итерируемый объект – это любой объект, набор элементов которого можно обрабатывать многократно и по-одному за раз. В Python все такие объекты реализуют метод __iter__() (возвращает итератор объекта) и/или __getitem__(index) (позволяет получать элементы объекта по индексу).
- Итератор – это специальный объект, в котором реализован метод __next__(), позволяющий при каждом новом вызове получать следующий элемент итерируемого объекта. Итераторы возвращаются, например, методом __iter__(), встроенной функцией iter(object), выражениями-генераторами и другими инструментами для их получения. Основным предназначением итераторов следует считать экономию памяти за счет генерации значений по одному за раз, а не использования всего набора значений сразу.
- Если говорить в общем, то итерируемый объект предоставляет нам некоторый набор элементов, а также инструмент в виде итератора для организации итераций. При этом некоторые встроенные объекты, например, файловые объекты, сразу поставляются в виде итераторов с уже реализованным методом __next__(). А вот, например, списки являются итерируемыми объектами, но не итераторами, поэтому метод __next__() у них отсутствует, а для получения итератора требуется вызов метода __iter__() или встроенной функции iter(). Это связано с тем, что списки предназначены для многократного использования с возможностью участия сразу в нескольких итерациях. Однако итераторы такой возможности не предоставляют, т.к. они относятся к одноразовым инструментам, которые с каждым вызовом метода __next__() опустошаются и становятся непригодными для дальнейших итераций.
-
Все доступные в Python итерационные инструменты используют в своей работе специальный протокол итераций,
который заключается в следующем:
- у переданного итерируемого объекта вызывается метод __iter__, который возвращает итератор объекта, хранящий в себе информацию о всех элементах итерируемого объекта и способный выдавать их по одному за раз;
- затем для получения каждого элемента итерируемого объекта у полученного итератора начинает вызываться метод __next__();
- каждый последующий вызов метода __next__() возвращает следующий элемент итерируемого объекта, что продолжается до тех пор, пока очередной вызов метода не возбудит исключение StopIteration;
- возбуждение исключения StopIteration приводит к остановке итераций и выходу из процесса.
- Выражение-генератор – это конструкция на базе цикла for, которая применяет заданное выражение к каждому элементу переданного итерируемого объекта в соответствии с заданным условием и затем возвращает объект-генератор со встроенным итератором, содержащий информацию о всех сформированных выражением элементах. Примеры выражений-генераторов: (str(x) for x in range(1, 5, 2)) (без условия) или (x**2 for x in [-5, -3, -2, -1] if x > -4 and x != -1) (генератор с условием). В отличие от генераторов списков, множеств и словарей, которые возвращают их целиком за раз, выражения-генераторы возвращают только итераторы итерируемых объектов, позволяя экономить память тем больше, чем больше размер последовательности значений.
- Функция-генератор – это специальная функция, которая использует инструкцию yield, способную приостанавливать выполнение функции с сохранением текущего состояния до следующего обращения к ней, и которая при вызове возвращает специальный объект-генератор со встроенным итератором, позволяющий по требованию возобновлять дальнейшее выполнение функции, начиная с момента последней приостановки процесса инструкцией yield. Так обычная функция def my_func(li): return li при вызове my_func([1, 2, 3]) вернет нам список целиком, в то время как функция-генератор def my_gen(li): yield li при вызове my_gen([1, 2, 3]) вернет нам только итератор, который будет выдавать нам при каждом вызове метода __next__() по одному элементу этого списка за раз. Опять же, на лицо экономия памяти, как и в случае со всеми другими типами итераторов.
Вопросы и задания для самоконтроля
1. Какие объекты мы называем итерируемыми? Относятся ли к ним строки и словари? Показать решение.
Ответ. Итерируемый объект – это любой объект, набор элементов которого можно обрабатывать многократно и по-одному за раз. В Python все такие объекты реализуют метод __iter__() (возвращает итератор объекта) и/или __getitem__(index) (позволяет получать элементы объекта по индексу).
Итерируемыми в Python являются все типы рассмотренных нами встроенных последовательностей (включая строки и словари), а также любые объекты, элементы которых можно обойти с помощью имеющихся в языке инструментов совершения итераций, например, уже известного нам цикла for.
2. Какие из представленных вариантов объектов итерируемы, а какие нет: [1, 2, 3], 123, '123', {1: '1', 2: '2', 3: '3'}, {True, False, None}, True, (False,), (None)? Проверьте себя программно. Показать решение.
Ответ. Итерируемые объекты: список [1, 2, 3], строка '123', словарь {1: '1', 2: '2', 3: '3'}, множество {True, False, None} и кортеж (False,). Числа, логические константы и тип None в Python неитерируемы.
Чтобы проверить наш ответ программно, нужно вспомнить, что в Python все итерируемые объекты реализуют метод __iter__() и/или __getitem__(index). Следовательно нам нужно просто проверить объекты на наличие данных методов (атрибутов объектов) при помощи встроенной функции hasattr(obj, attr_name).
# Проверим объекты на __iter__ .
print('[1, 2, 3]:', hasattr([1, 2, 3], '__iter__'))
print('123:', hasattr(123, '__iter__'))
print("'123':", hasattr('123', '__iter__'))
obj_1 = {1: '1', 2: '2', 3: '3'}
print("{1: '1', 2: '2', 3: '3'}:", hasattr(obj_1, '__iter__'))
obj_2 = {True, False, None}
print('{True, False, None}:', hasattr(obj_2, '__iter__'))
print('True:', hasattr(True, '__iter__'))
print('(False,):', hasattr((False,), '__iter__'))
print('(None):', hasattr((None), '__iter__'))
[1, 2, 3]: True
123: False
'123': True
{1: '1', 2: '2', 3: '3'}: True
{True, False, None}: True
True: False
(False,): True
(None): False
3. Дайте определение итератору. Какой из вызовов встроенной функции iter() вернет итератор объекта: iter(True) или iter([True])? Показать решение.
Ответ. Итератор – это специальный объект, в котором реализован метод __next__(), позволяющий при каждом новом вызове получать следующий элемент итерируемого объекта. Итераторы возвращаются, например, методом __iter__(), встроенной функцией iter(object), выражениями-генераторами и другими инструментами для их получения.
Вызов iter([True]) вернет итератор списка, а в результате вызова iter(True) мы получим исключение TypeError, т.к. логический тип данных неитерируем в Python.
4. Опишите протокол итераций в Python, после чего осуществите ручную итерацию по списку li = [1, 2, 3], выводя на экран через пробел значения списка, возведенные в квадрат. Показать решение.
Ответ. Протокол итераций еще раз можно посмотреть в кратких итогах параграфа. Что касается кода решения, то для организации итераций по списку я использовал удобные встроенные функции iter(obj) и next(iterator[, default]). В качестве дополнительного задания самостоятельно организуйте итерации по тому же списку, но уже с помощью соответствующих методов __iter__ и __next__.
# Создаем список.
li = [1, 2, 3]
# Получаем итератор списка посредством функции.
li_iter = iter(li)
# Получаем эл-ты с помощью функции next().
print(next(li_iter)**2, end=' ')
print(next(li_iter)**2, end=' ')
print(next(li_iter)**2, end=' ')
# Вместо StopIteration выведет 'The end!'.
print(next(li_iter, 'The end!'))
1 4 9 The end!
5. В чем заключается главное отличие выражений-генераторов от генераторов списков, множеств и словарей? Каково их главное предназначение? Показать решение.
Ответ. Выражения-генераторы возвращают итераторы, в то время как генераторы списков, множеств и словарей возвращают не итераторы, а уже готовые последовательности, заполненные сгенерированными значениями. Первостепенным предназначением выражений-генераторов является экономия памяти в ходе работы программ.
6. Какие генераторы представляют следующие фрагменты кода: (abs(x) for x in [-1, 0, 1]), [abs(x) for x in (-1, 0, 1)], {abs(x) for x in [-1, 0, 1]}, [abs(x) for x in {-1, 0, 1}], {abs(x): str(abs(x)) for x in [-1, 0, 1]}? Проверьте свои выводы программно. Показать решение.
Ответ. (abs(x) for x in [-1, 0, 1]) – выражение-генератор, [abs(x) for x in (-1, 0, 1)] – генератор списка, {abs(x) for x in [-1, 0, 1]} – генератор множества, [abs(x) for x in {-1, 0, 1}] – генератор списка, {abs(x): str(abs(x)) for x in [-1, 0, 1]} – генератор словаря.
# <class 'generator'>.
print(type((abs(x) for x in [-1, 0, 1])))
# <class 'list'>.
print(type([abs(x) for x in (-1, 0, 1)]))
# <class 'set'>.
print(type({abs(x) for x in [-1, 0, 1]}))
# <class 'list'>.
print(type([abs(x) for x in {-1, 0, 1}]))
# <class 'dict'>.
print(type({abs(x): str(abs(x)) for x in [-1, 0, 1]}))
<class 'generator'>
<class 'list'>
<class 'set'>
<class 'list'>
<class 'dict'>
7. Чем функция-генератор отличается от обычной функции? Показать решение.
Ответ. Главным отличием функций-генераторов от обычных функций является наличие в их теле инструкции yield. Во всем остальном это обычные функции, с той лишь разницей, что функции-генераторы интерпретатор компилирует так, чтобы они возвращали объект-генератор (итератор).
8. Для чего нужна инструкция yield? Показать решение.
Ответ. При наличии инструкции yield интерпретатор компилирует функцию как генератор. При вызове такая функция возвращает объект-генератор со встроенным итератором. Далее, при каждом вызове метода __next__() полученного итератора, функция-генератор запускается на выполнение вплоть до инструкции yield, которая возвращает результат вызывающей программе и приостанавливает выполнение функции с сохранением ее текущего состояния. При последующих вызовах метода __next__() со стороны вызывающей программы функция-генератор снимается с паузы и продолжает выполнение инструкций с места приостановки, т.е. с инструкции, которая следует за инструкцией yield.
9. Исправьте в коде все ошибки так, чтобы скрипт заработал. Показать решение.
# Импортируем модуль стандартной библиотеки.
import sys
# Проверяем наличие метода __iter__.
print(''abc'.__iter__:', hasattr('abc', '__iter__'))
# Получаем итератор списка.
li_iter = iter([1, 2)]
Получаем размер итератора.
print('Размер li_iter:', sys.getsizeof(li_iter))
# Протокол итераций в ручном режиме.
print(next(li_iter), End=' ')
print(next(li_itter), End=' ')
print(next(li_iter, 'End'))
# Выражение-генератор.
gen = (x**2, for x in range(2, 10, 3): if x > 4)
print(type(gen))
# Источник ключей и значений для словаря.
li = [('one': 1), ('two': 2), ('three': 3)]
# Генератор словаря.
d = {k, v for k, v in li}
# Выводим количество элементов словаря.
print(len(d))
# Определяем функцию-генератор.
def gen_func():
# Запускаем цикл.
for i in range(5): yeild i
# Получаем объект-генератор (итератор).
gen_obj = gen_func()
# Выводим тип объекта.
print(type(gen-obj))
# Импортируем модуль стандартной библиотеки.
import sys
# Проверяем наличие метода __iter__.
print("'abc'.__iter__:", hasattr('abc', '__iter__'))
# Получаем итератор списка.
li_iter = iter([1, 2])
# Получаем размер итератора.
print('Размер li_iter:', sys.getsizeof(li_iter))
# Протокол итераций в ручном режиме.
print(next(li_iter), end=' ')
print(next(li_iter), end=' ')
print(next(li_iter, 'End'))
# Выражение-генератор.
gen = (x**2 for x in range(2, 10, 3) if x > 4)
print(type(gen))
# Источник ключей и значений для словаря.
li = [('one', 1), ('two', 2), ('three', 3)]
# Генератор словаря.
d = {k: v for k, v in li}
# Выводим количество элементов словаря.
print(len(d))
# Определяем функцию-генератор.
def gen_func():
# Запускаем цикл.
for i in range(5): yield i
# Получаем объект-генератор (итератор).
gen_obj = gen_func()
# Выводим тип объекта.
print(type(gen_obj))
'abc'.__iter__: True
Размер li_iter: 48
1 2 End
<class 'generator'>
3
<class 'generator'>
10. Дополнительные тесты по теме расположены в разделе «Итерации и генераторы» нашего сборника тестов.
11. Дополнительные упражнения и задачи по теме расположены в разделе «Итерации и генераторы» нашего сборника задач и упражнений.
Быстрый переход к другим страницам
- Циклы for и while в Python
- Итерации и генераторы в Python
- Исключения и ошибки в Python
- К содержанию учебника