Байткод в Python

image
# пример для проведения декомпиляции
x = 30
y = 62
z = x + y

# python3.8 -m dis test.py

'''
  1           0 LOAD_CONST               0 (30)
              2 STORE_NAME               0 (x)
  2           4 LOAD_CONST               1 (62)
              6 STORE_NAME               1 (y)
  3           8 LOAD_NAME                0 (x)
             10 LOAD_NAME                1 (y)
             12 BINARY_ADD
             14 STORE_NAME               2 (z)
             16 LOAD_CONST               2 (None)
             18 RETURN_VALUE
'''

Когда интерпретатор СPython исполняет программу, он сначала ее транслирует в последовательность байткодовых инструкций.

Байткод — это промежуточный язык для виртуальной машины Python, который используется в качестве оптимизации производительности.

Байткод, который получается в результате этого шага компиляции, кэшируется на диске в файлах .pyc и .pyo, чтобы во второй раз исполнение того же самого файла Python проходило быстрее.

def greet(name):
    return 'Привет, ' + name + '!'
greet('Гвидо')
'Привет, Гвидо!'

Каждая функция имеет атрибут __code__, который мы можем использовать, чтобы получить инструкции виртуальной машины, константы и переменные, используемые нашей функцией greet:

greet.__code__.co_code
b'd\x01|\x00\x17\x00d\x02\x17\x00S\x00'
greet.__code__.co_consts
# https://docs.python.org/3/library/inspect.html#types-and-members
(None, 'Привет, ', '!')
greet.__code__.co_varnames
('name',)

co_consts содержит части строки приветствия, которую собирает наша функция. Константы и код хранятся отдельно, чтобы сэкономить пространство памяти.

Python хранит константы отдельно в поисковой таблице. Поток команд затем может ссылаться на константу по индексу в поисковой таблице.

Дизассемблер байткода Python располагается в модуле dis (документация), который является составной частью стандартной библиотеки. Поэтому мы можем его просто импортировать и вызвать dis.dis() с функцией greet в качестве аргумента, чтобы получить более удобочитаемое представление о ее байткоде:

import dis
dis.dis(greet)
  2           0 LOAD_CONST               1 ('Привет, ')
              2 LOAD_FAST                0 (name)
              4 BINARY_ADD
              6 LOAD_CONST               2 ('!')
              8 BINARY_ADD
             10 RETURN_VALUE

Главное, что сделал дизассемблер, было разбиение потока команд и назначение каждому находящемуся в нем коду операции человекочитаемого имени, как, например, LOAD_CONST.

Ссылки на константы и переменные теперь чередуются с байткодом и выведены полностью, чтобы уберечь нас от мозговой гимнастики относительно поиска по таблице co_const или co_varnames.

Глядя на человекочитаемые коды операций, мы начинаем понимать, как Python представляет и исполняет выражение 'Привет, ' + name + '!' в исходной функции greet().

Сначала он извлекает константу в индексе 1 ('Привет, ') и помещает ее в стек. Затем он загружает содержимое переменной name и также помещает ее в стек.

Стек является структурой данных, которая используется в качестве внутренней рабочей памяти виртуальной машины. Существуют разные классы виртуальных машин, и один из них называется стековой машиной. Виртуальная машина Python является реализацией такой стековой машины.

Самое интересное относительно стека как абстрактной структуры данных состоит в том, что на минимальном уровне он поддерживает всего две операции: вталкивание (push) и выталкивание (pop).

Вталкивание добавляет значение на вершину стека, а выталкивание удаляет и возвращает самое верхнее значение. В отличие от массива, в стеке отсутствует способ получить доступ к элементам «ниже» верхнего уровня.

Давайте предположим, что вначале стек пустой. После того как первые два кода операции были исполнены, содержимое стека виртуальной машины будет выглядеть следующим образом (0 — это самый верхний элемент):

0: 'Гвидо' (содержимое "name")
1: 'Привет, '

Инструкция BINARY_ADD выталкивает два строковых значения из стека, конкатенирует их, а затем вталкивает результат снова в стек:

0: 'Привет, Гвидо'

Затем идет еще одна инструкция LOAD_CONST, которая помещает в стек строку с восклицательным знаком:

0: '!'
1: 'Привет, Гвидо'

Следующий код операции BINARY_ADD снова объединяет два значения, чтобы сгенерировать заключительную приветственную строку:

0: 'Привет, Гвидо!'

Последняя байткодовая инструкция — RETURN_VALUE, которая сообщает виртуальной машине следующее: то, что в настоящее время находится на вершине стека, является возвращаемым значением этой функции, и поэтому оно может быть передано источнику вызова.

Дополнительное чтение:

Understanding Python Bytecode

Блокнот к статье