Наши партнеры

UnixForum





Библиотека сайта rus-linux.net

Интерпретатор языка Python, написанный на языке Python

Оригинал: A Python Interpreter Written in Python
Автор: Allison Kaptur
Дата публикации: July 12, 2016
Перевод: Н.Ромоданов
Дата перевода: февраль 2017 г.

Это четвертая часть статьи "Интерпретатор языка Python, написанный на языке Python".
Перейти к
началу.

Фреймы

К этому моменту мы узнали, что виртуальная машина языка Python является стековой машиной. Она может последовательно двигаться от текущей инструкции к следующей или переходить на произвольную инструкцию, помещать значения в стек и выталкивать их оттуда. Но у нас в нашей ментальной модели все еще есть некоторые недоработки. В приведенных выше примерах, последней инструкцией является инструкция RETURN_VALUE, которая соответствует выражению return исходного кода. Но куда происходит возврат?

Чтобы ответить на этот вопрос, мы должны увеличить сложность модели с помощью еще одной конструкции: фрейма. Фрейм представляет собой совокупность информации и контекста для некоторого куска кода. Фреймы создаются и уничтожаются на лету по мере того, как выполняется ваш код. Для каждого вызова функции есть связанный с ним соответствующий фрейм, так что хотя для каждого фрейма есть связанный с ним объект кода, внутри объекта кола может быть много фреймов. Если у вас есть функция, которая вызывала сама себя рекурсивно десять раз, то вам потребуется одиннадцать фреймов - по одному для каждого уровня рекурсии и один для модуля, с которого вы начали выполнение. В общем, в программе на языке Python для каждой области видимости есть некоторый фрейм. Например, для каждого модуля, для каждого вызова функции и для каждого определение класса есть некоторый фрейм.

Фреймы помещаются в стек вызовов, который является совершенно другим стеком, отличающимся от того стека, который мы рассматривали до этого момента. Стек вызовов является стеком, с которым вы, по большей части, уже знакомы – вы выдели, как он распечатывается при трассировке возникающих исключений. Каждая строка в результате трассировки, начинающаяся с текста "File 'program.py', line 10" ("Файл 'program.py', строка 10") соответствует одному фрейму в стеке вызовов. Стек, который мы рассматривали до этого, т. е. стек, с которым интерпретатор работает во время выполнения байт-кода, мы будем называть стеком данных. Также есть и третий стек, который называется стеком блоков. Блоками пользуются в некоторых случаях при управлении порядком ыполнения инструкций, в частности, в случае зацикливания и при обработке исключений. Каждый фрейм в стеке вызовов имеет свой собственный стек данных и стек блоков.

Давайте со всем этим разберемся на конкретном примере. Предположим, что интерпретатор языка Python в настоящее время выполняет строку с меткой 3, которая показана ниже. Интерпретатор находится в середине вызова функции foo, которая, в свою очередь, вызывает функцию bar. На рисунке показана схема стека вызовов фреймов, стеков блоков и стеков данных. Этот код написан для сессии REPL, поэтому мы сначала определяем необходимые функции. В тот момент, которым мы интересуемся, интерпретатор выполняет функцию foo(), находящуюся ниже, затем происходит обращение к телу функции foo, а затем выше – к функции bar.

>>> def bar(y):
...     z = y + 3     # <--- (3) ... и интерпретатор находится здесь
...     return z
...
>>> def foo():
...     a = 1
...     b = 2
...     return a + bar(b) # <--- (2) ... возврат вызова в bar ...
...
>>> foo()             # <--- (1) Мы в середине обращения к foo ...

Рис.12.2. Стек вызовов

В этом месте интерпретатор находится в середине вызова функции в bar. В стеке вызовов имеется три фрейма: один для уровня модуля, один для функции foo и один для функции bar (рис.12.2). После возвращения из функции bar, фрейм, ассоциированный с этой функцией, извлекается из стека вызовов и выбрасывается.

Инструкция байт-кода RETURN_VALUE сообщает интерпретатору о том, что между фреймами необходимо передать значение. Сначала она вытолкнет верхнее значение из стека данных верхнего фрейма на стеке вызовов. Затем он выталкивает весь фрейм из стека вызовов и выбрасывает его. И, наконец, значение помещается в стек данных в следующий фрейм, который был расположен ниже.

Когда Нед Батчелдер и я работали над интерпретатором Byterun, то в нашей реализации долго присутствовала одна существенная ошибка. Вместо того, чтобы иметь отдельный стек данных в каждом фрейме, у нас был только один стек данных для всей виртуальной машины. У нас были десятки тестов, составленных из маленьких фрагментов кода на языке Python, которые мы обрабатывали с помощью Byterun и с помощью реального интерпретатора языка Python с тем, чтобы убедиться, что в обоих интерпретаторах происходит одно и тоже. Проходили почти все эти тесты. Единственное, что у нас не работало, это - генераторы. И, наконец, при более внимательном чтении кода CPython, мы разобрались с этой ошибкой [2]. Перемещение стека данных в каждый фрейм устранило проблему.

Оглядываясь на эту ошибку, я была поражена тем, как мало в языке Python зависит от того, что в других фреймах хранится в стеке данных. Почти все операции интерпретатора языка Python тщательно очищают стек данных, поэтому тот факт, что все фреймы пользовались одним и тем же стеком, значения не имел. В приведенном выше примере, когда функция bar заканчивает выполнение, она оставляет свой стек данных пустым. Даже если бы функция foo использовала тот же самый стек, все значения лежали бы ниже. Но это не годится в случае с генераторами, ключевой особенностью которых является возможность перевести фрейм в паузу и перейти к какому-нибудь другому фрейму, а затем позже вернуться к фрейму генератора и получить его в том же самом состоянии, в котором его оставили.

Перейти к следующей части статьи.

Перейти к началу статьи.