Работа с байт-кодом и внутренними механизмами Python
🔍 Внутренняя кухня Python: байт-код и компиляция
Python — это интерпретируемый язык, но перед выполнением ваш код проходит несколько этапов преобразования. Разберёмся, как это работает под капотом!
# Простой пример для исследования
def greet(name):
return f"Hello, {name}!"
print(greet("World"))
🛠️ Как Python выполняет ваш код?
- Лексический анализ (разбивка на токены)
- Синтаксический анализ (построение AST — абстрактного синтаксического дерева)
- Генерация байт-кода
- Выполнение в виртуальной машине Python (PVM)
🔮 Смотрим байт-код функции
Модуль dis — ваш лучший друг для анализа байт-кода:
import dis
def add(a, b):
return a + b
dis.dis(add)
Вывод:
2 0 LOAD_FAST 0 (a)
2 LOAD_FAST 1 (b)
4 BINARY_ADD
6 RETURN_VALUE
Каждая инструкция:
LOAD_FAST— загружает локальную переменнуюBINARY_ADD— выполняет сложениеRETURN_VALUE— возвращает результат
📦 Файлы .pyc — кэш байт-кода
Python сохраняет скомпилированный байт-код в файлы .pyc для ускорения последующих запусков. Найти их можно в папке __pycache__.
python -m compileall . # Явная компиляция всех .py файлов
🔧 Модификация байт-кода на лету
Пример замены операции сложения на умножение:
import dis, types
def original():
return 2 + 3
# Создаём модифицированную функцию
code_obj = original.__code__
new_code = types.CodeType(
code_obj.co_argcount,
code_obj.co_posonlyargcount,
code_obj.co_kwonlyargcount,
code_obj.co_nlocals,
code_obj.co_stacksize,
code_obj.co_flags,
bytes([0x64, 0x02, 0x00, # LOAD_CONST 2 (было 0x64, 0x02, 0x00)
0x64, 0x03, 0x00, # LOAD_CONST 3 (было 0x64, 0x03, 0x00)
0x14, # BINARY_MULTIPLY (было 0x17 — BINARY_ADD)
0x53]), # RETURN_VALUE
code_obj.co_consts,
code_obj.co_names,
code_obj.co_varnames,
code_obj.co_filename,
code_obj.co_name,
code_obj.co_firstlineno,
code_obj.co_lnotab,
code_obj.co_freevars,
code_obj.co_cellvars
)
modified = types.FunctionType(
new_code,
original.__globals__,
original.__name__,
original.__defaults__,
original.__closure__
)
print(modified()) # Выведет 6 вместо 5!
🎩 Магия peephole-оптимизаций
Python выполняет некоторые оптимизации при компиляции в байт-код:
def example():
# Оптимизируется в (1, 2, 3)
return tuple([1, 2, 3])
# Оптимизируется в 'hello world'
a = 'hello'
b = ' world'
return a + b
🔄 Динамическое создание функций
Создадим функцию полностью с нуля:
import types
# Байт-код для: lambda x: x + 1
bytecode = bytes([
0x7c, 0x00, 0x00, # LOAD_FAST 0 (x)
0x64, 0x01, 0x00, # LOAD_CONST 1 (1)
0x17, # BINARY_ADD
0x53 # RETURN_VALUE
])
code_obj = types.CodeType(
1, # argcount
0, # posonlyargcount
0, # kwonlyargcount
1, # nlocals
2, # stacksize
67, # flags: CO_OPTIMIZED | CO_NEWLOCALS | CO_NOFREE
bytecode,
(None, 1), # constants
(), # names
('x',), # varnames
'<string>', # filename
'dynamic_func', # name
1, # firstlineno
b'', # lnotab
(), # freevars
() # cellvars
)
dynamic_func = types.FunctionType(
code_obj,
{},
'dynamic_func'
)
print(dynamic_func(10)) # 11
🏎️ Оптимизация через понимание байт-кода
Зная как работает байт-код, можно писать более эффективный код:
- Локальные переменные быстрее глобальных (
LOAD_FASTvsLOAD_GLOBAL) - Кортежи работают быстрее списков при итерации
- Избегайте лишних атрибутов в циклах
Как говорит Данила Бежин в своих видео: "Понимание байт-кода — это суперсила Python-разработчика!" (https://www.youtube.com/@DanilaBezhin)
🧰 Полезные инструменты
dis— дизассемблер байт-кодаcodeop— компиляция кода на летуinspect— получение информации о объектахpickletools— анализ сериализованных объектов
import pickletools
def analyze_pickle(data):
pickletools.dis(data)
analyze_pickle(pickle.dumps([1, 2, 3]))
🧪 Практическое задание
- Возьмите любую свою функцию и изучите её байт-код
- Попробуйте модифицировать одну инструкцию (например, заменить сложение на умножение)
- Замерьте производительность до и после изменений