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

от автора

По утверждению Роберта Мартина, объектно-ориентированный подход предложили в 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 идентификаторы.

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


ссылка на оригинал статьи https://habr.com/ru/articles/934742/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *