Генераторы#
Генератором в Python называется некоторый итерируемый объект. Если это вам мало, о чём говорит, то стоит изучить тему Итераторы.
В данном разделе коснёмся темы генераторов, так как они лежат в основе библиотеки дискретно-событийного моделирования SimPy, которую мы будем использовать в процессе обучения. Поэтому генераторы необходимо освоить, чтобы легко программировать имитационные модели любой сложности. Более того, поняв генераторы, вы легче поймёте асинхронное программирование, которое широко применяется в Web-разработке.
Что такое генератор#
Генератор - это разновидность итератора, который возвращается генераторной функцией.
Генераторная функция - это функция, в которой есть оператор yield
, отвечающий за возвращение объекта генератора.
Пример простейшей генераторной функции:
def gen_func():
yield
g = gen_func()
g
<generator object gen_func at 0x000001B485FC3ED0>
Чтобы получить значение из генератора, необходимо знать, что генератор как вид итератора поддерживает протокол итерации:
next(g)
Note
Кажется, что функция next
ни к чему не привела, но это не так.
Просто в данном случае yield
не имеет возвращаемой величины.
Вот, что будет, если к yield
добавить возвращаемое значение:
def gen_func():
# Хотим, чтобы генераторная функция давала
# генератор, выдающий одно целое число
yield 1
g = gen_func()
next(g)
1
Important
Генераторная функция возвращает итератор.
Как известно, когда итератор достигает своего конца, он инициирует исключение StopIteration
.
То же самое случится, если мы ещё раз применим next
к генератору g
:
# Для окраски ошибок и прочего в сочные цвета
from termcolor import cprint
try:
next(g)
except Exception as ex:
cprint(ex.__class__.__name__, "red")
---------------------------------------------------------------------------
ModuleNotFoundError Traceback (most recent call last)
Cell In[4], line 2
1 # Для окраски ошибок и прочего в сочные цвета
----> 2 from termcolor import cprint
5 try:
6 next(g)
ModuleNotFoundError: No module named 'termcolor'
Но раз генератор поддерживает протокол итератора, значит, он может использоваться в цикле for
, который этот протокол реализует:
g = gen_func()
for i in g:
print(i)
1
Или проще:
for i in gen_func():
print(i)
1
Таковы основы, однако созданный генератор имеет мало смысла.
Создадим, например, генератор, возвращающий последовательно числа от 0 до заданного n
:
def first_n(n):
counter = 0
while counter < n:
# Генерируем значение
yield counter
# Увеличиваем счётчик (движемся по циклу)
counter += 1
# Пример использования
for i in first_n(5):
print(i, end=" ")
0 1 2 3 4
Кое-что напоминает? К примеру, это:
for i in range(5):
print(i, end=" ")
0 1 2 3 4
Разберём по шагам, что происходило в цикле for
для first_n
:
for i in first_n(5):
print(i, end=" ")
Вызвана генераторная функция
first_n(5)
, вернувшая объект генератора:
g = first_n(5)
g
<generator object first_n at 0x7fcce1788350>
При этом внутри first_n
инициализировани счётчик counter = 0
и поток выполнения вошёл в цикл while
.
Дойдя до yield
, поток выполнения вернулся в точку вызова first_n
, т.е. в следующую за g = first_n(...)
строку.
К генератору применена функция
next
. Поток упраления переходит в функциюfirst_n
, но не в начало, а сразу же к операторуyield
, который генерирует текущее значениеcounter
. Цикл вfirst_n
, повторяясь, снова доходит до оператораyield
. Поток управления возвращается в место вызоваnext
. Результат сработавшегоyield
сохраняется в счётчикеi
:
i = next(g)
Вывод на экран значения
i
:
print(i)
0
Цикл повторяется с п.2:
# Итерация 2
i = next(g)
print(i)
# 3...
i = next(g)
print(i)
# И т.д., пока не закончатся значения в генераторе
1
2
Когда генератор будет исчерпан - а это случится, когда в first_n
завершится цикл while
и в ней не останется активного оператора yield
, - инициируется исключение StopIteration
, говорящее о завершении протокола итератора:
# 3...
i = next(g)
print(i)
# 4...
i = next(g)
print(i)
# И... конец
try:
next(g)
except Exception as ex:
cprint(ex.__class__.__name__, "red")
3
4
StopIteration
Генераторное выражение#
Есть ещё один способ создания генератора - генераторное выражение:
g = (i for i in range(5))
g
<generator object <genexpr> at 0x7fcce1788740>
Как видите, это не кортеж и не множество, а именно генератор.
Следовательно, никаких чисел от 0 до 5 в памяти компьютера нет.
И всё также сработает протокол итерации для g
:
for i in g:
print(i, end=" ")
0 1 2 3 4
Однако, мы можем использовать генераторы для конструирования списков, кортежей и колекций других типов. Для этого достаточно передать генератор соответствующему конструктору:
# Список из генератора так (через списковое выражение)...
a = [i for i in range(5)]
print("a =", a)
# или так (через конструктор)
b = list(i for i in range(5))
print("b =", b)
# Кортеж из генератора (только через конструктор)
c = tuple(i for i in range(5))
print("c =", c)
# Множество также только через конструктор
d = set(i for i in range(5))
print("d =", d)
a = [0, 1, 2, 3, 4]
b = [0, 1, 2, 3, 4]
c = (0, 1, 2, 3, 4)
d = {0, 1, 2, 3, 4}
Примеры генераторов#
В данном разделе содержатся примеры использования генераторов. В реальном коде изобретать велосипед не стоит - стандартные генераторы и функции работы с ними гораздо надёжнее и быстрее собственного решения.
Собственная версия range
#
В предыдущей главе Итераторы мы сделали собственную реализацию стандартного range
через итерируемый класс.
Здесь же реализуем ту же функциональность с помощью генератора:
# Генераторная функция, аналогичная range
def range_gen(start=0, stop=None, step=1):
i = start
if stop is None:
stop= start
start = 0
while i < stop:
yield i
i += step
# И пример его использования
print("range_gen:", end=" ")
for i in range_gen(3, 10, 2):
print(i, end=" ")
# Сравните со стандартным range
print("\nrange:", end=" ")
for i in range(3, 10, 2):
print(i, end=" ")
range_gen: 3 5 7 9
range: 3 5 7 9
На самом деле, кроме оператора yield
существует ещё оператор (выражение) yield from
.
Его предназначение - связать два генератора.
Рассмотрим, что это значит на примере нашего range_gen
:
def range_gen(start=0, stop=None, step=1):
if stop is None:
stop= start
start = 0
# Наш генератор генерирует значения,
# генерируемые другим генератором -
# это так называемая композиция генераторов
yield from range(start, stop, step)
# Использование
for i in range_gen(3, 10, 2):
print(i, end=" ")
3 5 7 9
Сравните с предыдущей версией и вы заметите, что теперь в генераторной функции не нужен счётчик i
, и что наш генератор берёт значения из стандартного range
. Таким способом можно связать сколь-угодное число генераторов.
Important
Однако помните, yield from
может запутать других и вас самих, если вы либо не хорошо понимаете предназначение данного выражения, либо если используете его неуместно.
Связывание генераторов без серьёзной на то причины усложняет код. И тут полезно вспомнить дзен Python: “Чем проще - тем лучше”.
Генератор бесконечной последовательности#
Одним из преимуществ генераторов является их ленивость - новое значение генератор выдаёт тогда и только тогда, когда оно запрашивается. Генератор не создаёт никаких предварительных списков, что положительно сказывается на работе программы с оперативной памятью. Более того, лень генераторов позволяет реализовать бесконечную последовательность:
def infinity():
i = 0
# Входим в бесконечный цикл
while True:
yield i
i += 1
# Аналог бесконечного цикла 'while True',
# только счётчик 'i' получается сам собой
for i in infinity():
if i == 5:
# Условие выхода из цикла.
# Не будь этого условия, числа выводились бы бесконечно
break
print(i, end=" ")
0 1 2 3 4
Для прерывания цикла выше использовался оператор break
.
Однако у генераторов, как и у любого объекта в Python, есть свои методы.
Один из методов - close
- предназначен для преждевременного закрытия генератора.
При этом генератор инициирует исключение StopIteration
, из-за чего цикл автоматически прервётся:
g = infinity()
print(next(g))
print(next(g))
g.close()
# С этого момента не стоит использовать генератор
try:
print(next(g))
except StopIteration:
cprint("StopIteration:", "red", end=" ")
print("да, инициировано исключение")
# Не будь try...except программа бы рухнула
0
1
StopIteration: да, инициировано исключение
Это позволяет в нашем случае использовать close
вместо break
следующим образом:
g = infinity()
for i in g:
if i == 5:
# Завершение цикла
g.close()
print(i, end=" ")
0 1 2 3 4 5
Собственная версия zip
#
# Наш zip тоже принимает произвольное число аргументов
def zip_gen(*sequences):
try:
# Заодно применим наш бесконечный генератор
# так удобный здесь
for i in infinity():
# Генерируем кортеж из i-ых элементов sequences
yield tuple(s[i] for s in sequences)
# Условие выхода из бесконечного цикла - ошибка индексации.
# Передаваемые списки могут иметь различные длины.
except IndexError:
# Да, генеративная функция обычная Python-функция,
# поэтому можно вернуться из неё обычным return'ом.
# Для генератора return сродни StopIteration.
return
a = [1, 2, 3, 4]
b = [-2, -3]
c = [7, 0, 11]
# Наш zip
print("zip_gen:")
for i, j, k in zip_gen(a, b, c):
print(i, j, k)
# Стандартный zip
print("zip:")
for i, j, k in zip(a, b, c):
print(i, j, k)
zip_gen:
1 -2 7
2 -3 0
zip:
1 -2 7
2 -3 0
Note
Заметьте, мы не создали какие-либо дополнительные списки.
Таким образом, концепция генераторов в Python имеет широкие возможности. При этом их синтаксис и логика действия предельно просты. Представленной информации вполне достаточно, чтобы научиться работать с библиотекой SimPy.
См. также#
Обучающий материал, подробно описывающий генераторы Python. Рассмотрены различные примеры их использования, а также особенности, не освещённые в данном справочнике. Например, методы генератора
send
,close
,throw
.Базовый материал по генераторам.