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

UnixForum





Библиотека сайта 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? А что насчет сложных списках?

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

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