Объекты кода Python: от компиляции до выполнения

Раздел: Продвинутый Python -> Внутреннее устройство 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, используя объекты кода.

Объекты кода Python - comments

En
Code object python (python)