Работа с байт-кодом и внутренними механизмами Python

🔍 Внутренняя кухня Python: байт-код и компиляция

Python — это интерпретируемый язык, но перед выполнением ваш код проходит несколько этапов преобразования. Разберёмся, как это работает под капотом!

# Простой пример для исследования
def greet(name):
    return f"Hello, {name}!"

print(greet("World"))

🛠️ Как Python выполняет ваш код?

  1. Лексический анализ (разбивка на токены)
  2. Синтаксический анализ (построение AST — абстрактного синтаксического дерева)
  3. Генерация байт-кода
  4. Выполнение в виртуальной машине 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

🏎️ Оптимизация через понимание байт-кода

Зная как работает байт-код, можно писать более эффективный код:

  1. Локальные переменные быстрее глобальных (LOAD_FAST vs LOAD_GLOBAL)
  2. Кортежи работают быстрее списков при итерации
  3. Избегайте лишних атрибутов в циклах

Как говорит Данила Бежин в своих видео: "Понимание байт-кода — это суперсила Python-разработчика!" (https://www.youtube.com/@DanilaBezhin)


🧰 Полезные инструменты

  1. dis — дизассемблер байт-кода
  2. codeop — компиляция кода на лету
  3. inspect — получение информации о объектах
  4. pickletools — анализ сериализованных объектов
import pickletools

def analyze_pickle(data):
    pickletools.dis(data)

analyze_pickle(pickle.dumps([1, 2, 3]))

🧪 Практическое задание

  1. Возьмите любую свою функцию и изучите её байт-код
  2. Попробуйте модифицировать одну инструкцию (например, заменить сложение на умножение)
  3. Замерьте производительность до и после изменений
Скрыть рекламу навсегда

🎥 YouTube: программирование простым языком

Канал, где я спокойно и по шагам объясняю сложные темы — без заумных терминов и лишней теории.

Подходит, если раньше «не заходило», но хочется наконец понять.

▶️ Смотреть курсы на YouTube