Генераторы в Python. Оператор yield. Генераторные выражения

Генераторы можно считать подвидом итераторов, а способ их создания – инструментом для создания несложных итераторов.

В отличие от обычных итераторов, генераторы создаются путем вызова функции, а не от класса.

Чтобы функция возвращала объект-генератор, в ее теле должен быть оператор yield. Когда любая yield-содержащая функция вызывается, она возвращает объект типа generator, а не None или какой-нибудь другой тип данных через оператор return.

У генераторов методы __next__ и __iter__ создаются средствами самого языка, то есть автоматически. Программисту их определять не надо, что упрощает создание пользовательских типов итераторов.

>>> def starmaker(n):
...     while n > 0:
...             yield '*'
...             n -= 1
... 
>>> type(starmaker)
>class 'function'>
>>> s = starmaker(3)
>>> type(s)
>class 'generator'>
>>> next(s)
'*'
>>> next(s)
'*'
>>> next(s)
'*'
>>> next(s)
Traceback (most recent call last):
  File ">stdin>", line 1, in >module>
StopIteration

В определенном смысле оператор yield заменяет return с тем исключением, что мы снова возвращаемся в функцию, когда вызывается next(). При этом объект-генератор помнит состояние переменных и место, откуда при прошлом вызове произошел выход из функции.

Если мы сделаем нечто подобное

>>> def g():
...     yield 1
... 

то не получим бесконечный генератор, потому что код тела функции полностью выполнится при первом вызове next():

>>> a = g()
>>> next(a)
1
>>> next(a)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

Обратите внимание, что функция starmaker делает то же самое, что класс, описанный в прошлом уроке:

>>> class A:
...     def __init__(self, qty):
...             self.qty = qty
...     def __iter__(self):
...             return self
...     def __next__(self):
...             if self.qty > 0:
...                     self.qty -= 1
...                     return '+'
...             else:
...                     raise StopIteration
... 
>>> a = A(3)
>>> for i in a:
...     print(i)
... 
+
+
+

При этом код функции, создающей итератор, намного короче аналогичного класса. Поэтому классы-итераторы скорее уместны, когда создаются сложные объекты, включающие множество полей и сложную логику их обработки, а не только методы __iter__ и __next__.

Генераторные выражения

Существует еще более простой, чем функция с yield, способ создания итераторов – генераторные выражения. Они подходят, когда код тела функции можно записать в одну строку.

Синтаксис генераторных выражений подобен генераторам списков, рассматриваемых в курсе "Python. Введение в программирование". Однако, в отличие от списков, в случае генераторов используются круглые скобки.

Напомним, как выглядят генераторы списков и то, что возвращают они списковый тип данных:

>>> a = [i+1 for i in range(10)]
>>> a
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
>>> type(a)
<class 'list'>
>>> import random
>>> b = [random.randint(0,9) for i in range(5)]
>>> b
[2, 5, 5, 2, 9]
>>> c = [i for i in b if i % 2 == 0]
>>> c
[2, 2]

Результат выражения, стоящего до for, добавляется на каждой итерации цикла в итоговый список. Выполнение выражения генератора списка сразу заполняет список.

В случае генераторных выражений создается объект-генератор, у которого будет вычисляться очередной элемент только при каждом вызове next():

>>> a = (i+1 for i in range(10))
>>> a
<generator object <genexpr> at 0x7fa586339f10>
>>> type(a)
<class 'generator'>
>>> next(a)
1
>>> next(a)
2

Пример со звездочкой с помощью генераторного выражения будет выглядеть так:

>>> d = ('*' for i in range(5))
>>> for i in d:
...     print(i)
... 
*
*
*
*
*

В отличие от генераторных выражений, yield-функции более универсальны не только из-за произвольного количества кода в их теле. В них вы можете передавать разные значения аргументов. А значит, одна и та же функция может использоваться для создания несколько разных генераторов.

Практическая работа

В задании к прошлому уроку требовалось написать класс-итератор, объекты которого генерируют случайные числа в количестве и в диапазоне, которые передаются в конструктор. Напишите выполняющую ту же задачу генераторную функцию. В качестве аргументов она должна принимать количество элементов и диапазон.

Курс с примерами решений практических работ:
pdf-версия


Объектно-ориентированное программирование на Python




Все разделы сайта