Рейтинг@Mail.ru
[Войти] [Зарегистрироваться]

Наши друзья и партнеры

UnixForum
Беспроводные выключатели nooLite купить дешевый 
компьютер родом из Dhgate.com

Lines Club

Ищем достойных соперников.

Библиотека сайта или "Мой Linux Documentation Project"

Интерпретатор языка 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? А что насчет сложных списках?

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

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



Эта статья еще не оценивалась
Вы сможете оценить статью и оставить комментарий, если войдете или зарегистрируетесь.
Только зарегистрированные пользователи могут оценивать и комментировать статьи.

Комментарии отсутствуют