Внутреннее устройство интерпретатора CPython
В этой статье я попытаюсь дать более полное представление о генераторах Python и о том, как они работают изнутри. Он будет включать как и почему. Разделы будут автономными, и вы можете пропускать их по своему желанию. Без лишних слов, давайте начнем наше путешествие. ⛵️
Что такое генераторы?
Генераторы, добавленные начиная с Python 2.2, представляют собой уникальные функции, которые можно приостанавливать, возобновлять и повторять. Давайте посмотрим на пример:
Определив функцию, содержащую ключевое слово yield
, функция помечается как генератор. Затем мы запускаем генератор в следующем порядке:
- Инициализируйте генератор и поместите его в
g
. На данный момент у нас есть новый объект-генератор, который еще не запущен. - Продвиньте генератор, позвонив ему
next()
. Это заставляет генератор перейти к первому ключевому словуyield
, по пути напечатав «Hello». - Снова продвиньте генератор. Он печатает «Goodbye», и, поскольку он достиг конца функции, вызывает
StopIteration
исключение и завершает работу.
Чтобы понять эти три простых шага, нам сначала нужно начать с самых основ того, как работает функция в Python.
Как работают функции
Запуск функции состоит из двух основных этапов: Инициализация и Оценка.
Инициализация функций
Под капотом CPython выполняет функцию внутри объекта frame.
Объект фрейма, иногда называемый стековым фреймом, состоит из следующих полей:
- объект кода, который включает фактический скомпилированный код функции.
- свободное пространство стека, называемое стеком значений, для запуска интерпретатора.
- указатель выполнения для текущей выполняемой строки кода.
- другие поля, такие как сопоставления локальных переменных.
Список всех полей фрейма можно увидеть в коде, а более короткий список можно увидеть в документации.
Когда мы инициализируем функцию, создается фрейм. В псевдокоде:
f = Frame()
Аргументы, которые мы передали функции, затем назначаются внутри фрейма:
f.f_locals["arg1"] = arg1 f.f_locals["arg2"] = arg2
Точка выполнения кадра установлена на первую строку кода в функции:
f.lasti = 0 # Last Instruction f.lineno = 0 # Function line number on file
На этом этапе наша функция запускается.
Беговые функции
Практически весь интерпретатор Python можно свести к одной функции уровня C: PyEval_EvalFrameEx.
Эта функция представляет собой цикл интерпретатора. Состоящий из 3k строк кода, его задача - оценивать фрейм или, другими словами, запускать его. Каждая функция в Python, каждый байт-код или код операции, выполняемые интерпретатором, проходят через эту функцию.
Во время «PyEval» Python работает с уникальным стеком значений фрейма. Эта информация будет иметь решающее значение позже.
Таким образом, когда вы вводите функцию и запускаете ее, интерпретатор создает фрейм и входит в цикл интерпретатора.
На сайте PythonTutor есть очень хороший интерактивный пример этого механизма.
Как работают генераторы
Как и в случае с функциями, запуск генератора включает этап инициализации и этап оценки.
Инициализация генератора
На этом этапе мы создаем объект-генератор. Объект-генератор состоит из объекта фрейма и объекта кода. Если вам интересно, да, объект фрейма также содержит внутри себя объект кода. В псевдокоде:
g = Generator() f = Frame() # Initialized as before g.gi_frame = f g.gi_code = f.f_code = Code() # The compiled function
Теперь наш генератор g
готов к работе.
Продвижение (запуск) генератора
Каждый раз, когда мы вызываем next(g)
, кадр оценивается с использованием того же PyEval_EvalFrameEx.
Разница в том, что при запуске генератора мы можем достичь ключевого слова yield
, после чего оценка кадра останавливается, а генератор «приостанавливается».
Указатель в кадре сохраняется, отмечая нашу текущую позицию в стеке, чтобы точно знать, в каком состоянии был генератор после остановки.
Важно помнить, что в отличие от функций, когда оценка останавливается, фрейм не уничтожается, поскольку он все еще прикреплен к объекту-генератору. Это позволит нам загрузить его позже.
PyEval возвращается, и наша вызывающая функция возобновляется.
Снова продвигаем генератор
Когда мы продвигаем генератор еще раз, кадр загружается из gi_frame
, и PyEval вызывается снова в том же кадре. Он проверяет, где мы остановились, переходит к нашему указателю выполнения и использует стек значений, хранящийся в кадре.
Поскольку мы используем один и тот же стек, а фрейм также содержит все локальные переменные, мы можем продолжить с того же места, как если бы функция никогда не останавливалась.
Когда мы доходим до конца генератора или оператора возврата, StopIteration
выбрасывается, и фрейм удаляется. gi_frame
устанавливается на None
.
Особенности генератора
Теперь, когда мы знаем, как генератор приостанавливается и возобновляется внутри, давайте рассмотрим еще несколько функций, которые делают генераторы действительно замечательными.
Урожайность ценностей
Генераторы могут выдавать значения, позволяя повторять их и возвращать результаты ленивым образом. Например:
Внутренне получение значений очень похоже на получение результата функции. Когда PyEval достигает кода операции YIELD_VALUE
, он выталкивает верхнее значение стека и возвращает его. Довольно просто, не правда ли?
Возвращение от генератора
Генераторы могут использовать ключевое слово return
для возврата результата:
Как видите, оператор return устанавливает исключение StopIteration
. Исключения могут иметь аргументы, и здесь первый аргумент, отправленный исключению StopIteration
, - это возвращаемое значение.
Возвращаемое значение можно получить, перехватив исключение и получив его первый аргумент:
Связь с генераторами
Мы также можем общаться с генераторами с помощью .send()
и .throw()
:
Как видите, мы отправили числа в генератор с помощью .send()
, и они были возвращаемым значением ключевого слова yield
.
Внутренне .send()
работает, помещая значение в верхнюю часть стека генератора. Затем он оценивает кадр и извлекает верхнее значение стека, помещая его в нашу локальную переменную.
Аналогичным образом .throw()
работает, отправляя throwflag
в PyEval, сообщая об исключении. Затем он нормально обрабатывает исключение. Если генератор не поймал его, исключение распространяется наружу, как обычная функция.
Вложенные генераторы
Генераторы могут быть вложенными (или делегированными) с использованием ключевого слова yield from
:
Как видите, использование yield from
создает способ связи между самым внутренним генератором, полностью.
Внутри он работает с использованием поля gi_yieldfrom
объекта-генератора. Он указывает на внутренний генератор, и когда вы используете .send()
, он будет работать до конца.
Когда внутренний генератор возвращается, он идет вверх по цепочке и соответственно устанавливает возвращаемое значение yield from
.
Заключение
В этой статье я дал некоторое представление о внутренней работе реализации генератора CPython. Хотя статья не предназначена для новичков, она лишь поверхностно затрагивает эту тему и позволяет понять, как работает интерпретатор, гораздо больше. Полная реализация, конечно же, с открытым исходным кодом, поэтому вы можете исследовать ее по своему желанию. Наслаждайтесь 😉