По утверждению Роберта Мартина, объектно-ориентированный подход предложили в 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
идентификаторы.
Возможно, именно этого и добивался Гвидо ван Россум?