По утверждению Роберта Мартина, объектно-ориентированный подход предложили в 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 "<stdin>", line 1, in <module> 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("yield", next(gen_short)) ... except StopIteration as error: ... print("return", repr(error.value)) ... yield 0 yield 1 yield 2 return None
Также как и в обычном случае, при завершении функции-генератора возвращается None. Как раз его нам и вывела инструкция print("return", repr(error.value)).
Заключение
Генераторная функция определяет объект генератора посредством синтаксиса объявления функций.
Объект-генератор удовлетворяет протоколу итератора и может быть использован во всех контекстах, требующих итерирования: инструкция for, операторы in и not in, конструкторы коллекций, встроенные функции next, iter и пр.
По сути, функция-генератор является конструктором, создающим объект при ее вызове. При этом, благодаря синтаксису функций, мы можем обращаться к состоянию объекта через переменные области видимости, без докучливой необходимости использовать для этого self идентификаторы.
Возможно, именно этого и добивался Гвидо ван Россум?
Спасибо всем кто дочитал до конца.
Вторую часть руководства по генераторам Вы найдете в статье
Python: Генераторы. Методы send, throw и close
