Объекты кода Python: от компиляции до выполнения
Объекты кода в Python: внутренняя структура и применение
Как получить и проанализировать объект кода существующей функции?
Наиболее прямой способ получить объект кода (code object) - использовать атрибут __code__ любой функции или метода. Объект кода содержит скомпилированный байткод, список констант, используемые имена, локальные переменные и другую метаинформацию.
def sample(a, b):
c = a + b
return c
code_obj = sample.__code__
print(type(code_obj))
print(code_obj.co_names)
print(code_obj.co_consts)
print(code_obj.co_varnames)Code object python (объекты кода python)
<class 'code'>
('a', 'b', 'c')
(None,)
('a', 'b', 'c')
Атрибут co_names содержит кортеж имён (переменные, функции, атрибуты), используемых в коде. co_consts хранит константы (числа, строки, вложенные кортежи), а co_varnames - локальные имена. Полный список атрибутов насчитывает более десятка полей, включая co_argcount, co_filename, co_code (сырой байткод) и другие.
Для анализа байткода удобно использовать модуль dis:
import dis
dis.dis(sample)
2 0 LOAD_FAST 0 (a)
2 LOAD_FAST 1 (b)
4 BINARY_ADD
6 STORE_FAST 2 (c)
3 8 LOAD_FAST 2 (c)
10 RETURN_VALUE
Такой анализ помогает понять, как интерпретатор выполняет код, и может быть полезен при оптимизации или отладке.
Как создать объект кода из строки с помощью compile()?
Функция compile() возвращает объект кода (или AST) из исходного кода. Второй аргумент - имя файла (строка), третий - режим компиляции: 'exec' (для целого модуля), 'eval' (для одиночного выражения) или 'single' (для одной инструкции).
source = "x = 10\ny = x * 2"
code_obj = compile(source, '<string>', 'exec')
print(code_obj)
print(code_obj.co_consts)
print(code_obj.co_names)
<code object <module> at 0x7f...>
(10, 2, None)
('x', 'y')
Объект кода, созданный через compile(), можно выполнить с помощью exec() или eval(). Это часто используется при динамическом выполнении кода, например, для реализации математических выражений в калькуляторах.
Как изменить объект кода функции для модификации поведения?
Некоторые атрибуты объекта кода можно изменять (например, co_consts), но это требует создания нового объекта через types.CodeType или замены __code__ у функции. Пример - замена константных значений в замыкании:
def original():
return 42
# Создадим новый объект кода с другой константой
new_code = original.__code__.replace(co_consts=(100,))
original.__code__ = new_code
print(original()) # Теперь возвращает 100
100
Метод replace() (Python 3.8+) возвращает новый code object с заменёнными полями. В более старых версиях приходится использовать types.CodeType.
Типичная ошибка: попытка изменить атрибут напрямую (original.__code__.co_consts = (100,)) приводит к AttributeError, потому что объект кода в большинстве реализаций (CPython) является неизменяемым. Используйте только replace() или создание нового объекта через конструктор CodeType.
Также важно следить за совместимостью: количество констант должно соответствовать ожиданиям байткода, иначе возможны сбои выполнения.
Как создать полностью новую функцию из объекта кода?
Можно сконструировать объект кода с нуля, используя класс types.CodeType, и затем обернуть его в объект функции через types.FunctionType. Это требует точного указания всех полей.
import types
# Минимальный code object для сложения двух чисел
code = types.CodeType(
2, # argcount
0, # posonlyargcount
0, # kwonlyargcount
2, # nlocals
64, # stacksize
1024, # flags (0x400 для GENERATOR? упростим)
b'|\x00\x00|\x01\x00\x17\x00S\x00', # co_code
(None,), # co_consts
(), # co_names
('a', 'b'), # co_varnames
'<string>', # co_filename
'add', # co_name
1, # co_firstlineno
b'', # co_lnotab
(), # co_freevars
() # co_cellvars
)
def add(a, b):
pass
add.__code__ = code
print(add(3, 4))
7
Конструктор CodeType принимает до 18 аргументов в Python 3.8+ (включая co_qualname и co_exceptiontable). Список аргументов меняется между версиями, поэтому такой подход требует осторожности.
Ошибки несоответствия версий: код, написанный под Python 3.10, может не сработать под 3.7 из-за другого количества обязательных полей. Рекомендуется использовать replace() вместо прямого конструирования, если это возможно.
Как объект кода используется при отладке и профайлинге?
Объекты кода хранят строку исходного файла (co_filename) и номер первой строки (co_firstlineno). С помощью inspect.getsource() и inspect.getsourcelines() можно восстановить исходный код, если файл доступен. Эти данные используются профайлерами (cProfile) и трейсерами (traceback) для построения стека вызовов.
import inspect
def foo():
return bar()
def bar():
return inspect.currentframe().f_code.co_name
print(foo())
bar
Атрибут co_freevars содержит имена свободных переменных в замыканиях, co_cellvars - переменные, захваченные вложенными функциями. Это позволяет анализировать области видимости.
Расширенные примеры работы с объектами кода
Рассмотрим несколько подробных, нестандартных примеров использования code object.
1. Визуализация байткода с разбором опкодов
import dis
code_str = "
def fact(n):
return 1 if n <= 1 else n * fact(n-1)
"
code_obj = compile(code_str, '', 'exec')
# Извлекаем объект кода функции (он лежит в co_consts[0])
func_code = code_obj.co_consts[0]
dis.dis(func_code)
2 0 LOAD_FAST 0 (n)
2 LOAD_CONST 1 (1)
4 COMPARE_OP 1 (<=)
6 POP_JUMP_IF_FALSE 12 (to 12)
8 LOAD_CONST 1 (1)
10 RETURN_VALUE
>> 12 LOAD_FAST 0 (n)
14 LOAD_GLOBAL 0 (fact)
16 LOAD_FAST 0 (n)
18 LOAD_CONST 1 (1)
20 BINARY_SUBTRACT
22 CALL_FUNCTION 1
24 BINARY_MULTIPLY
26 RETURN_VALUE
Интерпретация: рекурсивная функция fact проверяет условие, потом рекурсивно вызывает себя. Видно, что глобальное имя fact хранится в co_names.
2. Динамическая замена констант с созданием новой версии функции
def add_five(x):
return x + 5
# Заменяем константу 5 на 10
new_code = add_five.__code__.replace(co_consts=(None, 10))
add_ten = types.FunctionType(new_code, globals(), 'add_ten')
print(add_ten(100)) # 110
110
Обратите внимание: co_consts первого элемента чаще всего None (для docstring), поэтому индекс константы 1. Заменяем кортеж целиком.
3. Создание code object с помощью compile() для разных режимов
# Режим 'eval' – для выражения
code_eval = compile('2+3', '', 'eval')
print(eval(code_eval))
# Режим 'single' – для одной инструкции (включает вывод в интерактивной оболочке)
code_single = compile('x = 1 + 2; x', '', 'single')
exec(code_single) # выведет 3 в интерактивном режиме
5
4. Манипуляции с байткодом напрямую (экспериментально)
import struct
def original():
x = 10
return x
# Прочитаем байткод: LOAD_CONST 0 (10), STORE_FAST 0 (x), LOAD_FAST 0 (x), RETURN_VALUE
code_bytes = original.__code__.co_code
print(code_bytes.hex())
# Изменим константу 10 на 20 через замену co_consts
new_consts = (None, 20) # (docstring, 20)
new_code = original.__code__.replace(co_consts=new_consts)
original.__code__ = new_code
print(original()) # 20
64006400... 20
Прямое редактирование байткода (co_code) – сложный путь, требующий знания опкодов. Проще менять константы или имена через replace().
5. Получение code object из класса
class MyClass:
def method(self):
pass
code_obj = MyClass.method.__code__
print(code_obj.co_varnames) # ('self',)
('self',)
6. Проверка наличия замыкания через co_freevars
def outer(x):
def inner():
return x
return inner
inner_func = outer(42)
print(inner_func.__code__.co_freevars) # ('x',)
('x',)
Эти примеры демонстрируют, насколько глубоко можно проникнуть во внутреннее устройство Python, используя объекты кода.