64. Генераторы и оператор yield в Python
Оглавление урока
- Генераторные функции и выражения
- Генераторные функции и оператор yield
- Генераторные выражения
- В дополнении про генерацию
Введение
В предыдущем уроке мы познакомились с функциональным программированием. В этом уроке продолжим наше исследование функций в языке Python и поговорим о генераторах. Вынужден вас огорчить, здесь мы тоже полностью не раскроем тему и вернемся к ней в уроках про ООП. Но не будем забегать вперед. Перед прочтением урока, освежите в своей памяти информацию из урока «List/dict/set comprehensions (включения) в Python».
Напомню, списковые и др. включения (list/dict/set comprehensions) более лаконичные и производительные, главное не переусердствовать и не превратить лаконичность в заумную неразбериху.
Генераторные функции и выражения
Python поддерживает откладывание, т.е. есть инструменты, которые позволяют выполнить операцию не сразу, а при необходимости. Вспомните предыдущий урок и пример про «фильтрацию» людей, кому меньше 18 лет, именно тот, где возвращается объект-генератор. В Python есть две конструкции, которые откладывают выполнение операций: генераторные функции и генераторные выражения. Обо всем по порядку.
Генераторные функции и оператор yield
Генераторные функции – такие же функции, создаваемые при помощи оператора def
, но в них присутствует оператор yield
, чтобы возвращать по одному результату за вызов и останавливать выполнение функции с сохранением состояния. За счет приостановки выполнения мы можем сэкономить пространство памяти и распределить время вычислений.
В уроке «Аргументы и параметры функций, операторы * и ** в Python» вы разобрались с оператором return
, который незамедлительно возвращает одиночный результат из функции. В Python возможно вернуть значение из функции и позднее продолжить с того места, которое было оставлено.
Генераторные функции – все те же функции, только возвращают объект-генератор, который поддерживает протокол итерации (если забыли про итерации, то вернитесь к уроку «Итераторы в Python»).
Если вы не вернулись к уроку про итераторы, я все равно напомню про протокол итерации. Объекты итераторов определяет метод __next__
, который возвращает очередной элемент итерации (при помощи функции next()
), либо, в случае его отсутствия, поднимает исключение StopIteration
(про исключения было в уроке «Обработка исключений (try/except) в Python»).
Функции, имеющие в своем составе оператор yield
компилируются особым образом как генераторы – они поддерживают протокол итерации и имеют метод __next__
, за счет которого мы можем с ними работать как с итераторами.
Метод __next__
генератора возобновляет работу до тех пор, пока не вернется следующий результат yield
или не будет вызвано исключение StopIteration
. Использование оператора return
в генераторной функции вызовет исключение StopIteration
, т.е. таким образом можно прекратить генерацию значений и выйти из функции.
Слишком много слов, переходим к практике. Создадим функцию, которая будет вычислять квадраты чисел от 0
до N
.
def square(N):
for i in range(1, N):
yield i * i
result = square(1000000)
print(next(result)) # => 1
print(next(result)) # => 4
print(next(result)) # => 9
Как видите, мы вызвали функцию square()
один раз, после несколько раз вызвали функцию next()
для объекта-генератора. В таких небольших и простых программах это кажется незначительным преимуществом, но не стоит забывать, программы имеют свойство разрастаться и тогда приходится думать, как сэкономить ресурсы.
Теперь углубимся в протокол генераторных функций. Кроме метода __next__
, есть еще метод send
, дающий возможность взаимодействовать с генератором и влиять на его работу. Оператор yield
мы можем превратить в выражение:
def square(N):
y = 0
for i in range(1, N):
y = yield i ** y
result = square(100)
next(result)
print(result.send(2)) # => 4
print(result.send(5)) # => 243
print(result.send(3)) # => 64
Таким образом, мы можем отправить в функцию степень, в которую мы хотим возвести очередное число. Главное не забываем запустить генератор при помощи функции next()
. При помощи метода send()
мы можем, например, передать код завершения, чтобы прекратить работу генератора.
В Python 3.3. появился расширенный оператор yield from
, который позволяет делегировать работу подгенератору. Эта информация выходит за рамки урока и приведена больше для расширения кругозора. Если вам интересно, как это работает, то вы всегда можете посмотреть в официальной документации.
Переходим ко второй конструкции, которая позволяет отложить выполнение операций.
Генераторные выражения
Генераторные функции оказались настолько удобные, что этот подход распространился и на другие инструменты. Помните, в уроке «List/dict/set comprehensions (включения) в Python» мы не написали про touple comprehensions. Потому что в круглых скобках записывается генераторное выражение. Помните, в предыдущем уроке мы заменили квадратные скобки спискового включения:
result = [x for x in result if x[1] >= 18]
На круглые скобки:
result = (x for x in result if x[1] >= 18)
И в переменной result
получили не кортеж, как логично можно было подумать, а объект-генератор. Это выражение в круглых скобках не что иное, как генераторное выражение.
Синтаксически генераторные выражения похожи на списковые включения, но только помещаются в круглые скобки. Главное отличие от спискового включения — это то, что будет храниться в памяти после выполнения. Списковые включения (list comprehensions) после выполнения сохраняют в памяти весь генерируемый список, в свою очередь, генераторное выражение (generator expression) сохраняет в память только объект-генератор, с которым в дальнейшем можно работать по необходимости.
Объект-генератор является итерируемым объектом, следовательно, поддерживает протокол итерации. Этот итерируемый объект все так же сохраняет состояние генератора.
На практике списковые включения могут выполняться гораздо быстрее генераторных выражений, поэтому последние стоит применять для больших наборов данных, которые не нужно генерировать все сразу. Пока все это голословно, но не беспокойтесь, скоро мы дойдем до инструментов для измерения времени выполнения кода.
В дополнении про генерацию
Важное примечание, касательно как генераторных функций, так и генераторных выражений – они являются объектами с одиночной итерацией. То есть мы не можем иметь несколько итераторов, находящихся в разных позициях одного набора данных.
На самом деле вы уже много раз встречались с генераторами: в словарях, файлах и много где еще. Помните урок «Аргументы и параметры функций, операторы * и ** в Python», в котором мы говорили про распаковку итерируемых объектов. Да, генераторное выражение тоже можно распаковать:
print(*(x for x in range(1, 100)))
Что в итоге, генераторы – сложный инструмент и необязательный в использовании. Так как весь язык Python пронизывают генераторы, то мы не могли упустить эту тему. К тому же, многие программисты используют генераторные выражения и функции в своем коде и незнание этих понятий может привести к недопониманию как при общении, так и при чтении чужого кода.
Главное не усложняйте свой код самописными генераторами, существование которых неоправданно.
К этой теме еще вернемся в контексте объектно-ориентированного программирования, а сейчас решите тест и переходите к уроку про оценочные мероприятия.