понедельник, 7 мая 2012 г.

Исследование объектов кода в Python




Вдохновленный выступлением David Beazley Keynote на PyCon, я решил на днях покопаться в кодовых объектах Python'a. У меня пока нет актуальных задач, для которых это могло бы понадобиться, так что воспринимайте это просто как смесь мыслей и бреда, который может быть Вам интересен (в противном случае - мои извинения.
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.




Источник

Комментариев нет:

Отправить комментарий