Disclaimer: В данной статье говорится о CPython версии 2.7, однако это может быть справедливо и для других версий CPython (включая 3.x). Но я не делаю никаких предположений о том, как это будет работать под PyPy, Jython, IronPython, и т.д..
Шаг 0: О чём вообще речь?
Для начала, что вообще такое объект кода? Многие (особенно ненавистники Python) утверждают, что Python является интерпретируемым языком, и всё же весь ваш код на Python перед исполнением компилируется. Это происходит даже если Вы вводите код в интерактивной оболочке. CPython содержит виртуальную машину, которая и исполняет stack-based байт-код. В процессе выполнения всё, что может быть исполнено (функции, методы, модули, тело классов, лямбды, инструкции, выражения и т.д.) исполняется именно как байт-код в виртуальной машине Python.Объект кода - это объект Python, который представляет кусок байт-кода вместе со всем, что необходимо для его исполнения: объявлением ожидаемых аргументов, их типов и количества, списком (не словарь! Об этом чуть позже) локальных переменных, информацией об источнике кода, из которого был получен байт-код (для отлаживания и вывода трассировки стека) и т.п. - ну и конечно (очевидно) сам байт-код в качестве
str
(или, в Python3, bytes
).Хотя объекты кода и представляют кусок исполняемого кода, тем не менее самих их исполнить напрямую невозможно. Для того, чтобы выполнить объект кода необходимо использовать либо ключевое слово
exec
либо функцию eval().
Шаг 1: Создадим объект кода...
По большей части Вы не встретитесь с объектами кода при программировании на Python. Или же они практически наверняка были созданы для Вас Python без того, чтобы Вы это вообще заметили. В некоторых случаях Вы можете захотеть и сами создать такие объекты, например для наших экспериментов:>>> code_str = """
... print "Hello, world"
... """
>>> code_obj = compile(code_str, '<string>', 'exec')
>>> code_obj
<code object <module> at 0x1054c74b0, file "<string>", line 2>
Ёшкин кот, наш первый код!Первый аргумент функции
compile()
это строка кода Python для компиляции, что, впрочем, очевидно. Второй аргумент определяет "имя файла" для данного кода (согласно соглашениям мы используем <string>
чтобы показать, что код получен из интерактивной оболочки). Третьим параметром является тип компиляции, который в большинстве случаев будет exec
, как и в нашем примере. Другими вариантами могут быть eval
, для строки, содержащей только одно выражение, или single
, для кода содержащего только один оператор, чье возвращаемое значение выводится на экран, если только оно не равно None
(как, например, в интерактивной оболочке).Если Вы используете тип компиляции
eval
, то если ваш код содержит операторы (как в нашем примере, где присутствует оператор print
), компиляция выдаст ошибку синтаксиса:>>> code_str = """
... print "Hello, world"
... """
>>> code_obj = compile(code_str, '<string>', 'eval')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<string>", line 2
print "Hello, world"
^
SyntaxError: invalid syntax
Когда Вы используете single
, обрабатывается только одно выражение; все остальные (а так же всё, что выражением не является) будут пропущены:>>> code_str = """
... print "Hello, world"
... print "Goodbye, world"
... """
>>> code_obj = compile(code_str, '<string>', 'single')
>>> exec code_obj
Hello, world
И где же мой “goodbye”?В оставшейся части поста я буду использовать
exec
, который используется Python`ом при импортировании модулей.Шаг 2: Вскрытие
Давайте вернёмся к первому примеру и посмотрим, что же внутри нашего объекта кода:>>> code_str = """
... print "Hello, world"
... """
>>> code_obj = compile(code_str, '<string>', 'exec')
>>> dir(code_obj)
# dunder атрибуты (с двойным нижним подчёркиванием) опущены для ясности
['co_argcount', 'co_cellvars', 'co_code', 'co_consts', 'co_filename',
'co_firstlineno', 'co_flags', 'co_freevars', 'co_lnotab', 'co_name',
'co_names', 'co_nlocals', 'co_stacksize', 'co_varnames']
Хотя эти атрибуты прокомментированы в inspect
модуле, я всё же освещу некоторые моменты:Во первых мы можем обнаружить тут наш второй аргумент функции
compile()
:>>> code_obj.co_filename
'<string>'
И, возможно это будет для Вас неожиданностью, наш код представляет из себя анонимный модуль (код, скомпилированный при помощи exec
всегда воспринимается как код уровня модуля, хотя он может содержать и определение функции, класса и любой другой код Python):>>> code_obj.co_name
'<module>'
И, как мы и ожидаем, объект кода, представляющий модуль Python (то, что у нас всего одна строка с одним оператором дела не меняет) не принимает ни одного аргумента:>>> code_obj.co_argcount
0
>>> code_obj.co_varnames
()
Если бы использовали код определения функции, которая принимает аргументы, мы бы увидели следующее:>>> def foo(x, y):
... print x, y
...
>>> foo.func_code
<code object foo at 0x1054b9830, file "<stdin>", line 1>
>>> foo.func_code.co_varnames
('x', 'y')
>>> foo.func_code.co_argcount
2
Если Вам очень интересно, то Вы можете посмотреть и сам байт-код, который обрабатывается виртуальной машиной Python:>>> code_obj.co_code
'd\x00\x00GHd\x01\x00S'
На мой взгляд не стоит пытаться заучивать этот язык, так как есть более лёгкий способ чтения байт-кода (мы увидим его в следующем разделе)И, наконец, у нас есть одна константа (строка “Hello, world”) в области видимости, которая выводится на экран:
>>> code_obj.co_consts
('Hello, world', None)
Секундочку.... А откуда тут взялся None
?!Нормальные герои всегда идут в обход или дизасемблер нам зачем?
Для того, чтобы понять, что же происходит в нашем объекте кода мы воспользуемся модулемdis
которые переводит байт-код в последовательность понятных для человека инструкций:>>> dis.dis(code_obj)
2 0 LOAD_CONST 0 ('Hello, world')
3 PRINT_ITEM
4 PRINT_NEWLINE
5 LOAD_CONST 1 (None)
8 RETURN_VALUE
Для чтения полученного результата требуется немного опыта, так что позвольте мне Вам помочь. Инструкция LOAD_CONST
читает значение из кортежа co_consts
и помещает её на вершину стека. Инструкция PRINT_ITEM
берёт значение с вершины стека и выводит его строковое представление. PRINT_NEWLINE
должно быть понятно само по себе.Снова мы видим этот загадочный
None
. На самом деле это особенность реализации виртуальной машины CPython. Так как вызовы функций в Python (включая "скрытые" вызовы функций, те, что стоят за инструкцией import
) являются реализацией вызова функций С в виртуальной машине Python, модули тоже имеют возвращаемое значение, которое сигнализирует виртуальной машине что выполнение модуля завершено и контроль может быть возвращён в вызвавшую область (т.е. в модуль с инструкцией import
). Я не хочу запутаться, объясняя это, так что если Вам интересно - смотрите Larry Hastingss PyCon презентацию Продираясь через CPython длительностью около 44:22 — там говорится о Python 3.x, но это же релевантно для Python 2.7. Если Вас интересуют такие детали реализации - Вам определённо стоит посмотреть это видео целиком, как и David Beazley’s keynote.Шаг 3: Интересные кишочки...
Большая часть того, о чём мы говорили полезна для виртуальной машины, но что с того нам, людям? Что если мы захотим отлаживать код в интерактивном режиме (используяpdb
или что-то в этом роде), или получить полезную и читаемую трассировку для ошибки?Оказывается, и это возможно! Как мы уже видели, объекты кода показывают, из какого файла они были получены, что, несомненно, поможет нам найти их исходный код. Кроме того, они так же сообщают строку, с которой начинается код для данного байт-кода.:
>>> code_obj.co_firstlineno
2
Для чего нужен загадочный атрибут co_lnotab
? Для того, чтобы проиллюстрировать отчет мы возьмём кусок кода чуть больше:>>> code_str = """
... x = 1
... y = 2
... print x + y
... """
>>> code_obj = compile(code_str, '<string>', 'exec')
>>> code_obj.co_lnotab
'\x06\x01\x06\x01'
Эмм... И чё? Может быть нам снова придёт на помощь модуль dis
:>>> dis.dis(code_obj)
2 0 LOAD_CONST 0 (1)
3 STORE_NAME 0 (x)
3 6 LOAD_CONST 1 (2)
9 STORE_NAME 1 (y)
4 12 LOAD_NAME 0 (x)
15 LOAD_NAME 1 (y)
18 BINARY_ADD
19 PRINT_ITEM
20 PRINT_NEWLINE
21 LOAD_CONST 2 (None)
24 RETURN_VALUE
Самый левый столбик - это номер линии кода из которого получен данный объект кода (обратите внимание, что 2 соответствует значению code_obj.co_firstlineno
). Следующий столбец - смещение в пределах байт-кода для каждой инструкции: 0 байтов для первой, 3 для второй и т.д. Третий столбец - сама инструкция, а четвёртый - аргумент этой инструкции, если есть, так же как и значение этого аргумента, указанное в скобках.Теперь мы можем сопоставить это с
co_lnotab
(что расшифровывается как “line number table” - "таблица номеров строк") для того чтобы понять как Python находит соответствие между объектом кода и исходным кодом:>>> code_obj.co_lnotab
'\x06\x01\x06\x01'
После недолгой борьбы методом проб и ошибок, я понял, что это набор пар байтов: первый байт в паре это значение смещения в байт-коде (6 байтов, где находится вторая инструкция LOAD_CONST
, как мы видим в нашей таблице), после чего идёт количество строк исходного кода, которым соответствует данная инструкция.Мы можем подтвердить наше предположение немного изменив наш исходный код, перекомпилировав его и проверив атрибут
co_lnotab
получившегося объекта кода:>>> code_str2 = """
... x = 1
...
... y = 2
... print x + y
... """
>>> code_obj2 = compile(code_str2, '<string>', 'exec')
>>> code_obj2.co_lnotab
'\x06\x02\x06\x01'
Мы сдвинули вторую операцию присваивания вниз на одну строчку и можем увидеть, что мы увеличили значение второго байта на единицу, так как теперь одной инструкцией мы охватываем не одну а две строки.Кроме того, мы можем убедиться, что оба байт-кода идентичны, не смотря на изменения в исходном коде:
>>> code_obj2.co_code == code_obj.co_code
True
Так как и смещение в байт-коде и смещение в исходном коде представлено единственным байтом (без знака), проницательный человек мог бы спросить: "А что будет если мы должны пропустить 257 строк?" Давайте посмотрим:>>> thousand_blanks = '\n' * 1000
>>> code_str = """
... x = 1
... """ + thousand_blanks + """
... y = 2
... print x + y
... """
>>> code_obj = compile(code_str, '<string>', 'exec')
>>> code_obj.co_lnotab
'\x06\xff\x00\xff\x00\xff\x00\xec\x06\x01'
Так как и смещение байт-кода и смещение строк являются лишь смещением, наличие большого количества пустых строк означает что некоторые смещения строк приводят к нулевому смещению байт-кода. Так и в нашем случае - сперва у нас есть 6-и байтовое смещение байт-кода, затем 255 строк смещения исходного кода и соответствующее ему отсутствующее смещения байт-кода, и т.д. Бинго!В заключение...
Для начала приношу извинения за сумбурный ход мыслей в этом посте, но я надеюсь, что было интересно. Продолжайте исследовать природуexec
и его использование в Keystone.
Комментариев нет:
Отправить комментарий