Генераторы#

Генератором в 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=" ")
  1. Вызвана генераторная функция 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(...) строку.

  1. К генератору применена функция next. Поток упраления переходит в функцию first_n, но не в начало, а сразу же к оператору yield, который генерирует текущее значение counter. Цикл в first_n, повторяясь, снова доходит до оператора yield. Поток управления возвращается в место вызова next. Результат сработавшего yield сохраняется в счётчике i:

i = next(g)
  1. Вывод на экран значения i:

print(i)
0
  1. Цикл повторяется с п.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.

См. также#

  1. Обучающий материал, подробно описывающий генераторы Python. Рассмотрены различные примеры их использования, а также особенности, не освещённые в данном справочнике. Например, методы генератора send, close, throw.

  2. Базовый материал по генераторам.