Как стать автором
Поиск
Написать публикацию
Обновить

Python: генераторные функции

Уровень сложностиПростой
Время на прочтение5 мин
Количество просмотров922

По утверждению Роберта Мартина, объектно-ориентированный подход предложили в 1966-м году Оле-Йохан Даль и Кристен Нюгор. Для эмуляции объектов они использовали возможность языка ALGOL, позволяющую переместить кадр стека вызова функции в динамическую память (кучу).

В этом смысле в 2001 году Гвидо ван Россум переизобрёл объекты, добавив Python 2.2 генераторные функции.

В Python функция становится функцией-генератором если в ней встречается выражение:

"yield" [expression]

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

Вызов генераторной функции

Определим функцию следующим образом:

>>> def generator_function():
...     print("begin")
...     yield
...  

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

>>> generator_instance = generator_function()
>>> generator_instance
<generator object generator_function at 0x7c3a8315bae0>
>>> type(generator)
<class 'generator'>

Как мы видим, вызов функции-генератора возвращает объект типа generator. Также обратите внимание, что не была напечатана строка "begin". То есть блок кода функции-генератора не начинает выполняться при ее вызове.

Давайте еще раз взглянем на generator_instance:

>>> iter(generator_instance) is generator_instance
True
>>> item = next(generator_instance)
begin

Да, перед нами итератор — так как его можно использовать со встроенными функциям iter и next, и при этом функция iter возвращает его самого.

Его итерирование начинает выполнять блок инструкций функции-генератора.
Мы видим напечатанную строку "begin".

Давайте посмотрим, что происходит дальше

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

Объявим элементарную генераторную функцию:

>>> def generator_function():
...     print("begin")
...     for i in range(3):
...         print("iteration #", i)
...         yield i
...         print("post yield")
...     print("end")
...

Сохраним возвращаемый ею объект-генератор и проитерируем его:

>>> generator_instance = generator_function()
>>> next(generator_instance)
begin
iteration # 0
0
>>> next(generator_instance)
post yield
iteration # 1
1
>>> next(generator_instance)
post yield
iteration # 2
2
>>> next(generator_instance)
post yield
end
Traceback (most recent call last):
  File "<pyshell#11>", line 1, in <module>
    next(generator_instance)
StopIteration

Первая итерация начинает выполнение блока инструкций функции-генератора. Оно продолжается до yield выражения, которое пожинает элемент итератора, возвращаемый функцией next.

Так как REPL автоматически выводит значения выражений, мы видим следующие строки.

Результат вызова функции print в теле функции-итератора:

iteration # 0

А это — вывод REPL’ом значения выражения next(generator_instance) — первого элемента итератора:

0

На следующей итерации функция-итератор продолжает своё выполнения с момента вычисления yield выражения. Она выполняется, пока не встретит следующее yield выражение, которое пожнёт новый элемент итератора.

В конце-концов мы выходим из цикла внутри функции и печатаем строку "end". После этого завершается выполнение функции-генератора, что приводит к возбуждению исключения StopIteration.

Контексты, в которых используются итераторы, сами перехватывают и обрабатывают исключение StopIteration — прозрачно для пользователя. Оно сигнализирует о необходимости завершить итерирование.

определенная ранее генераторная функция
>>> def generator_function():
...     print("begin")
...     for i in range(3):
...         print("iteration #", i)
...         yield i
...         print("post yield")
...     print("end")
...

Использование генератора в цикле for

>>> for i in generator_function():
...     print("item", i)
...
begin
iteration # 0
item 0
post yield
iteration # 1
item 1
post yield
iteration # 2
item 2
post yield
end

Использование генератора в конструкторе list

>>> list(generator_function())
begin
iteration # 0
post yield
iteration # 1
post yield
iteration # 2
post yield
end
[0, 1, 2]

Параметры генератора

Генераторные функции могут принимать параметры — также как и обычные функции.
Значения параметров передаются в функцию-генератор в момент ее вызова — то есть во время создания объекта-генератора.

Напишем генератор, который пожинает только цифры из переданной ему строки.

>>> def digits(string):
...     for ch in string:
...         if ch.isdecimal():
...             yield ch
...

Проверим как работает написанный генератора

>>> list(digits("Mar 21"))
['2', '1']

Пустое yield-выражение

При записи yield выражения мы может не указывать никакого выражения после ключевого слова yield. В этом случае генераторная функция пожнёт None значение.

Перепишем предыдущий пример так, чтобы каждому символу исходной строки поставить в соответствовал элемент итератора. Цифры мы оставим без изменения, а для других символов пожнём значение None.

>>> def digits(string):
...     for ch in string:
...         if ch.isdecimal():
...             yield ch
...         else:
...             yield
...

Проверим работу генератора на том же примере

>>> list(digits("Mar 21"))
[None, None, None, None, '2', '1']

Инструкция return и атрибут value объекта StopIteration

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

Проверим это на примере, объявив следующую функцию-генератор

>>> def generator_function(limit):
...     for i in range(limit):
...         yield i
...         if i % 4 == 3:
...             return "That's all Folks"
... 

В качестве параметра передадим число побольше, чтобы инструкция return точно сработала. Число 100 подойдет:

>>> gen_long = generator_function(100)
>>> try:
...     while True:
...         print("yield", next(gen_long))
... except StopIteration as error:
...     print("return", repr(error.value))
... 
yield 0
yield 1
yield 2
yield 3
return "That's all Folks"

А теперь выберем такое число, при котором цикл внутри функции завершится до того как выполнится инструкция return. Число 3, например:

>>> gen_short = generator_function(3)
>>> try:
...    while True:
...        print(next(gen_short))
... except StopIteration as error:
...    print(error.value)
...   
0
1
2
None

Также как и в обычном случае, при завершении функции-генератора возвращается None. Как раз его нам и вывела инструкция print(error.value).

Заключение

Генераторная функция определяет объект-генератора посредством синтаксиса объявления функций.

Объект-генератор удовлетворяет протоколу итератора и может быть использован во всех контекстах, требующих итерирования: инструкция for, операторы in и not in, конструкторы коллекций, встроенные функции next, iter и пр.

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

Возможно, именно этого и добивался Гвидо ван Россум?

Теги:
Хабы:
+1
Комментарии3

Публикации

Ближайшие события