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