Библиотека сайта rus-linux.net
Интерпретатор языка Python, написанный на языке Python
Оригинал: A Python Interpreter Written in Python
Автор: Allison Kaptur
Дата публикации: July 12, 2016
Перевод: Н.Ромоданов
Дата перевода: февраль 2017 г.
Это третья часть статьи "Интерпретатор языка Python, написанный на языке Python".
Перейти к
началу.
Настоящий байт-код языка Python
Теперь мы откажемся от наших наборов игрушечных инструкций и перейдем к реальному байт-коду языка Python. Структура байт-кода похожа на инструкции нашего игрушечного интерпретатора, за исключением лишь того, что для идентификации каждой команды используется один байт, а не длинное имя. Чтобы разобраться с этой структурой, давайте рассмотрим байт-код небольшой функции. Рассмотрим следующий пример:
>>> def cond(): ... x = 3 ... if x < 5: ... return 'yes' ... else: ... return 'no' ...
В языке Python можно на этапе выполнения программы получать доступ ко всем его внутренним компонентам, так что мы можем получить доступ к ним прямо из REPL. Для объекта функции cond
, атрибут cond.__code__
представляет собой объект кода, ассоциированный с этой функцией, а атрибут cond.__code__.co_code
является байт-кодом. Непосредственный доступ к этим атрибутам при создании кода языке Python почти всегда будет плохим решением, но эти атрибуты позволяют посмотреть на содержимое функции и попытаться с ним разобраться.
>>> cond.__code__.co_code # байт-код в виде байтов b'd\x01\x00}\x00\x00|\x00\x00d\x02\x00k\x00\x00r\x16\x00d\x03\x00Sd\x04\x00Sd\x00 \x00S' >>> list(cond.__code__.co_code) # байт-код в виде чисел [100, 1, 0, 125, 0, 0, 124, 0, 0, 100, 2, 0, 107, 0, 0, 114, 22, 0, 100, 3, 0, 83, 100, 4, 0, 83, 100, 0, 0, 83]
Когда мы просто выдаем байт-код, он выглядит неразборчиво - все, что мы можем сказать о нем, что это последовательность байтов. К счастью, для того, чтобы разобраться в байт-кодете, мы можем воспользоваться мощным инструментальным средством: модулем dis
из стандартной библиотеки языка Python.
Модуль dis
является дизассемблером байт-кода. Дизассемблер берет низкоуровневый код, написанный для машин и похожий на код на языке ассемблера, и выводит его в удобочитаемом виде. Когда мы запускаем метод dis.dis
, то для байт-кода выводится пояснение.
>>> dis.dis(cond) 2 0 LOAD_CONST 1 (3) 3 STORE_FAST 0 (x) 3 6 LOAD_FAST 0 (x) 9 LOAD_CONST 2 (5) 12 COMPARE_OP 0 (<) 15 POP_JUMP_IF_FALSE 22 4 18 LOAD_CONST 3 ('yes') 21 RETURN_VALUE 6 >> 22 LOAD_CONST 4 ('no') 25 RETURN_VALUE 26 LOAD_CONST 0 (None) 29 RETURN_VALUE
Что все это означает? Давайте в качестве примера посмотрим на первую инструкцию LOAD_CONST. Число в первом столбце (2) показывает номер строки в нашем исходном коде на языке Python. Второй столбец представляет собой указатель байт-кода, сообщающий нам, что инструкция LOAD_CONST
размещается в нулевой позиции. Третья колонка является самой инструкцией, преобразованной в удобочитаемый вид. Четвертый столбец, если он присутствует, является аргументом этой инструкции. Пятый столбец, если он также присутствует, помогает нам разобраться, что это за аргумент.
Рассмотрим первые несколько байтов этого байт-кода: [100, 1, 0, 125, 0, 0]. Эти шесть байтов представляют собой две команды с их аргументами. Мы можем воспользоваться методом dis.opname для того, чтобы преобразовать эти байты в более понятный вид и выяснить, что означают инструкции 100 и 125:
>>> dis.opname[100] 'LOAD_CONST' >>> dis.opname[125] 'STORE_FAST'
Второй и третий байты - 1, 0 - аргументы инструкции LOAD_CONST
, а пятый и шестой байты - 0, 0 - аргументы инструкции STORE_FAST
. Точно так же, как в нашем игрушечном примере, инструкции LOAD_CONST
нужно знать, где найти константу, которую следует загрузить, а инструкции STORE_FAST
нужно найти имя переменной, где будет храниться значение. Инструкция LOAD_CONST
в языке Python является точно такой же, как инструкция LOAD_VALUE
нашего игрушечного интерпретатора, а инструкция LOAD_FAST
точно такой же, как инструкция LOAD_NAME
. Таким образом, эти шесть байтов представляют собой первую строку кода, х = 3. Зачем использовать два байта для каждого аргумента? Если бы в языкы Python для поиска констант и имен переменных использовался только один байт вместо двух, то у нас в одном объекте кода могло бы быть только 256 имен / констант. Использование двух байтов позволило иметь до 256 в квадрате, или 65536, имен или переменных.
Условные выражения и циклы
До сих пор, интерпретатор выполнял код просто пошагово переходя от одной инструкции к следующей. В этом проблема; часто нам нужно выполнить определенные инструкции много раз, или пропускать их при определенных условиях. Для того, чтобы мы могли в нашем коде писать циклы и инструкции if, интерпретатор должен уметь переходить на любую инструкцию. В некотором смысле, в байт-коде языке Python циклы и условные выражения обрабатываются с помощью операторов GOTO
! Снова взгляните на результат дизассемблирования функции cond
:
>>> dis.dis(cond) 2 0 LOAD_CONST 1 (3) 3 STORE_FAST 0 (x) 3 6 LOAD_FAST 0 (x) 9 LOAD_CONST 2 (5) 12 COMPARE_OP 0 (<) 15 POP_JUMP_IF_FALSE 22 4 18 LOAD_CONST 3 ('yes') 21 RETURN_VALUE 6 >> 22 LOAD_CONST 4 ('no') 25 RETURN_VALUE 26 LOAD_CONST 0 (None) 29 RETURN_VALUE
Условное выражение if x < 5
из строки 3 нашего кода компилируется в четыре инструкции: LOAD_FAST
, LOAD_CONST
, COMPARE_OP
и POP_JUMP_IF_FALSE
. Для фрагмента x < 5
генерируется код загрузки значения x
, загрузки значения 5 и сравнения этих двух значений. Инструкция POP_JUMP_IF_FALSE
отвечает за реализацию операции if
. Эта команда вытолкнет из стека интерпретатора верхнее значение. Если это значение истинно, то ничего не происходит. Значение может быть "истинным" – оно не должно быть литеральным объектом True
. Если значение ложно, то интерпретатор выполнит переход к другой иструкции.
Инструкция, куда осуществляется переход, называется целевой инструкцией, и она указывается в качестве аргумента команды POP_JUMP
. Здесь целевой инструкцией указана 22. Инструкцией с указателем 22 будет инструкция LOAD_CONST
, расположенная в стоке 6. Модуль dis
помечает такие целевые инструкции символами ">>". Если результатом сравнения x < 5
будет ложь, то интерпретатор перейдет непосредственно на строку 6 (return "no"
), пропустив строку 4 (return "yes"
). Таким образом, интерпретатор использует команд перехода для того, чтобы выборочно пропускать некоторые части набора инструкций.
Команды переходов также используются для реализации циклов языка Python. В приведенном ниже байт-коде обратите внимание на то, что для строки while x < 5
генерируется байт-код, почти идентичный байт-коду строки if x < 10
. В обоих случаях вычисляется сравнение, а затем с помощью POP_JUMP_IF_FALSE
выбирается, какая инструкция будет выполняться следующей. В конце строки 4, т.е. в конце тела цикла, инструкция JUMP_ABSOLUTE
всегда перенаправляет интерпретатор обратно к инструкции 9, которая находится в верхней части цикла. Когда х < 5
становится ложным, инструкция POP_JUMP_IF_FALSE
перенаправляет интерпретатор мимо конца цикла, к инструкции 34.
>>> def loop(): ... x = 1 ... while x < 5: ... x = x + 1 ... return x ... >>> dis.dis(loop) 2 0 LOAD_CONST 1 (1) 3 STORE_FAST 0 (x) 3 6 SETUP_LOOP 26 (to 35) >> 9 LOAD_FAST 0 (x) 12 LOAD_CONST 2 (5) 15 COMPARE_OP 0 (<) 18 POP_JUMP_IF_FALSE 34 4 21 LOAD_FAST 0 (x) 24 LOAD_CONST 1 (1) 27 BINARY_ADD 28 STORE_FAST 0 (x) 31 JUMP_ABSOLUTE 9 >> 34 POP_BLOCK 5 >> 35 LOAD_FAST 0 (x) 38 RETURN_VALUE
Изучаем байткод
Я призываю вас пытаться применять дизассемблер dis.dis
к функциям, которые вы пишете. Некоторые вопросы для дальнейшего изучения:
- В чем в интерпретаторе языка Python разница между циклом for и циклом while?
- Каким образом можно написать отличающиеся функции, для которых будет сгенерирован идентичный байт-код?
- Как работает инструкция elif? А что насчет сложных списках?
Перейти к следующей части статьи.
Перейти к началу статьи.