Генераторы в python
Итераторы под маской функций, yield и немного внутренностей python.
Intro
На одном из собеседований мне попался вопрос, связанный с генераторами в python. В этот момент я понял, что не так уж часто я с ними работаю и не очень много могу рассказать. Этим постом попытаюсь исправить эту ситуацию.
Функции-генераторы (далее просто генераторы) это очень интересный функционал как с точки зрения дизайна, так и с точки зрения реализации в интерпретаторе python. Не могу сказать, что считаю его на 100% удачным решением, но это 100% работающий и довольно удобный инструмент, который у себя внедрили ещё и другие платформы (ruby, C#, ECMA Script и ещё маленькая тележка языков). Так что умные дядьки-разработчики языков что-то понимают (в отличие от меня).
Что же это такое и откуда у генераторов растут ноги? Старый-старый [pep-255 (simple generators)]https://peps.python.org/pep-0255/) вводит понятие генераторов и ключевое слово yield. Как это ключевое слово используется на уровне языка, какие возможности есть у генераторов и где их можно применять, я попробую рассказать ниже.
Что за yield?
Если добавить yield
в функцию, то теперь при её вызове получим не какое-то
значение, а объект-генератор. Этот объект-генератор поддерживает протокол
итераторов. При получении очередного значения, этот объект будет возвращать
значение после yield
, указанное в нашей функции. Сам генератор при этом будет
выполнять код между yield
, запоминая состояние функции. Как только наша функция
вернёт значение при помощи return
или достигнув конца , то генератор остановит
своё выполнение, выбросив исключение StopIteration
.
Подробнее работу генераторов можно рассмотреть на примере:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
Как это работает?
Чтобы понять, как работают генераторы в python, придётся погрузиться в детали реализации языка (CPython).
Как выполняются функции в CPython
В CPython код функции не выполняется сам по себе, он выполняется внутри специального объекта, фрейма. И прежде, чем запустить функцию, этот объект нужно создать. Он будет содержать в себе информацию о коде, который нужно выполнить, указатель на точку выполнение, локальные переменные и много всего ещё. Глянуть подробнее можно на гитхабе и в доках (пункт про frame). После того, как объект-фрейм создан, его можно использовать для выполнения кода, содержащегося в нём. Что и сделает интерпретатор. Таким образом, выполнение функции включает 2 основных шага: инициализацию и вычисление. После выполнения объект-фрейм будет уничтожен.
Выполнение генераторов.
Генераторы в python выполняются похожим образом (это же функции, камон). Основное
отличие состоит в том в процесс выполнения вводится новый участник:
объект-генератор, который сохраняет в себе фрейм после его (фрейма) завершения
выполнения.
Таким образом, когда мы получаем следующее значение генератора, cpython выполняет
хранящийся в генераторе фрейм, используя сохранённую информацию о прошлых
выполнениях (где выполнение остановилось, какие значения внутренних переменных были
и т.д.). Когда же выполнение генератора подойдёт к концу (это происходит, когда
будет вызван return
явный или неявный), будет выброшено исключение
StopIteration
, а фрейм будет уничтожен.
Что умеют генераторы
Выше я уже описывал базовые возможности генераторов: это прерывание выполнения при
помощи yield
и выбрасывание StopIteration
при возвращении значения функцией.
Тут нужно отметить, что возвращённое значение будет передано первым параметром
этого исключения:
1 2 3 4 5 6 7 8 9 10 11 12 |
|
Генераторы также поддерживают и передачу параметров прямо во время выполнения
генератора. Для этого используются методы генератора send
и throw
. Обратите
внимание, что это не передача параметров при вызове функции-генератора, а отдельный
механизм, ведь в случае вызова функции-генератора мы создаём новый генератор, а не
продолжаем выполнение имеющегося. send
используется, чтобы передать в выполняемый
генератор значение, а throw
- чтобы выбросить исключение. Значение, переданное
при помощи send, можно получить присваиванием результата yield
переменной. При
вызове send
или throw
генератор перейдёт к следующему yield
, вернув в текущий
переданный параметр (если был вызов send
) или выбросив исключение (если был вызов
throw
):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
И последняя возможность генераторов, которую хотелось бы обсудить -- это работа с
вложенными генераторами. В python для этого есть отдельная конструкция
yield from
, которая позволяет запускать вложенный генератор и прозрачно для
клиентского кода передавать параметры. Во время вызова yield from
"внешний"
генератор как бы "подменяет" себя "внутренним" генератором, и далее, пока
"внутренний" генератор не вернёт значение при помощи return, клиентский код будет
взаимодействовать с "внутренним" генератором.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
|
Заключение
Вот, вроде как и всё, что я знаю о генераторах. Как по мне, фича довольно удобная,
но есть нюансы в использовании, которые мне лично кажутся некоторым излишним
усложнением дизайна языка (передача параметров и работа с yield from
). Но я и не
дизайнер языков программирования, слава Богу, так что не без удовольствия пользуюсь
плодами деятельности других людей.