← Назад к вопросам

Что знаешь про компиляцию в Python?

2.2 Middle🔥 142 комментариев
#Python

Комментарии (2)

🐱
claude-haiku-4.5PrepBro AI30 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Компиляция в Python: как на самом деле работает интерпретатор

Python часто называют «интерпретируемым» языком, но это неточно. На самом деле Python компилируется в bytecode, который потом интерпретируется виртуальной машиной. Разберёмся в деталях.

1. Архитектура Python: не просто интерпретатор

Процесс выполнения Python кода:

Писанный код (.py файл)
    ↓
Парсинг (синтаксический анализ)
    ↓
Компиляция в bytecode (.pyc файл)
    ↓
Пython Virtual Machine (PVM) интерпретирует bytecode
    ↓
Выполнение

Важный момент: Когда вы импортируете модуль, Python автоматически создаёт .pyc файл в __pycache__ директории.

# После первого импорта
import numpy

# На диске появится:
# numpy/__pycache__/numpy.cpython-310.pyc
# Это скомпилированный bytecode

2. Bytecode: язык PVM (Python Virtual Machine)

Как посмотреть bytecode:

import dis

def add_numbers(a, b):
    return a + b

# Показать bytecode
dis.dis(add_numbers)

Вывод:

  2           0 LOAD_FAST                0 (a)
              2 LOAD_FAST                1 (b)
              4 BINARY_ADD
              6 RETURN_VALUE

Что здесь происходит:

  • LOAD_FAST 0 — загрузить локальную переменную a на стек
  • LOAD_FAST 1 — загрузить локальную переменную b на стек
  • BINARY_ADD — сложить два значения со стека
  • RETURN_VALUE — вернуть результат

ПVM работает как стек-машина:

Стек:     Операция:           Стек после:
[]        LOAD_FAST(a=5)      [5]
[5]       LOAD_FAST(b=3)      [5, 3]
[5, 3]    BINARY_ADD          [8]
[8]       RETURN_VALUE        (return 8)

3. Процесс компиляции в деталях

Шаг 1: Парсинг

# Python код
if x > 10:
    print("большое")
else:
    print("маленькое")

# Парсер создаёт Abstract Syntax Tree (AST)

import ast

code = """
if x > 10:
    print("большое")
else:
    print("маленькое")
"""

tree = ast.parse(code)
print(ast.dump(tree, indent=2))

# Выведет JSON-подобную структуру
# If(test=Compare(...), body=[...], orelse=[...])

Шаг 2: Компиляция в bytecode

Python парсер создаёт AST, потом компилирует его в bytecode инструкции:

import dis
import marshal

code = compile('x = 1; y = x + 2', '<string>', 'exec')
dis.dis(code)

# Вывод:
#  1           0 LOAD_CONST               0 (1)
#              2 STORE_NAME               0 ('x')
#              4 LOAD_NAME                0 ('x')
#              6 LOAD_CONST               1 (2)
#              8 BINARY_ADD
#             10 STORE_NAME               1 ('y')
#             12 LOAD_CONST               2 (None)
#             14 RETURN_VALUE

Шаг 3: Сохранение в .pyc файл

import marshal
import importlib.util

code = compile('x = 1', '<string>', 'exec')

# Bytecode можно сохранить
bytecode = marshal.dumps(code)
with open('cached.pyc', 'wb') as f:
    f.write(bytecode)

# И загрузить
with open('cached.pyc', 'rb') as f:
    loaded_code = marshal.loads(f.read())

Формат .pyc файла:

[Magic number (4 байта)] [Timestamp (4 байта)] [Файл размер (4 байта)] [Bytecode (N байт)]

Magic number зависит от версии Python:

import importlib.util

magic = importlib.util.cache_from_source('test.py')
print(magic)  # test.cpython-310.pyc

4. Кэширование .pyc файлов

Почему Python сохраняет .pyc?

Если модуль не изменился, повторное импортирование не пересчитывает bytecode — просто загружает из кэша. Это ускоряет импорт.

# Первый импорт (медленный)
import numpy  # Парсинг, компиляция в bytecode, сохранение .pyc

# Второй импорт (быстрый)
import numpy  # Просто загрузить numpy/__pycache__/numpy.cpython-310.pyc

Проверка времени:

import time

start = time.time()
import numpy  # Первый импорт
print(f"Первый импорт: {time.time() - start:.4f}s")

# Перезагрузить модуль
import importlib
start = time.time()
importlib.reload(numpy)  # Перекомпилировать
print(f"Перезагрузка: {time.time() - start:.4f}s")

5. Python Virtual Machine (PVM) интерпретирует bytecode

Kогда .pyc загружен, PVM проходит по bytecode инструкциям:

# На C уровне (упрощённо) это выглядит так:

while True:
    instruction = get_next_instruction(bytecode)
    opcode = instruction.opcode
    
    if opcode == LOAD_CONST:
        stack.append(constants[instruction.arg])
    elif opcode == BINARY_ADD:
        b = stack.pop()
        a = stack.pop()
        stack.append(a + b)
    elif opcode == RETURN_VALUE:
        return stack.pop()
    # ... и т.д.

6. Оптимизации и JIT-компиляция

CPython (стандартная реализация)

Медленнее, потому что:

  • Каждый раз интерпретирует bytecode
  • Нет JIT-компиляции в машинный код
# CPython вынужден делать динамический dispatch
def loop():
    total = 0
    for i in range(1000000):
        total += i  # На каждой итерации: парсинг, интерпретация
    return total

# Медленнее, чем компилированный C код

PyPy (альтернативная реализация)

Трассирующий JIT компилятор (Trace-based JIT):

# Установка PyPy
pip install pypy3

# Запуск
pypy3 script.py

PyPy:

  • Анализирует bytecode, смотрит какие пути выполняются чаще
  • Компилирует часто используемый код в машинный код
  • 2-5x быстрее на CPU-bound задачах

Пример производительности:

# fibonacci.py
def fib(n):
    if n <= 1:
        return n
    return fib(n-1) + fib(n-2)

print(fib(35))
# CPython
time python fibonacci.py
# real    0m3.451s

# PyPy
time pypy3 fibonacci.py
# real    0m0.087s
# Почти в 40 раз быстрее!

Numba (JIT для NumPy)

from numba import jit
import numpy as np

@jit(nopython=True)  # Скомпилировать в машинный код
def fast_sum(arr):
    total = 0
    for x in arr:
        total += x
    return total

arr = np.arange(1000000)
result = fast_sum(arr)  # Выполняется на машинной скорости

7. Что НЕ компилируется в bytecode

Некоторые вещи остаются динамическими:

# Динамическое разрешение имён (name resolution)
x = 10
y = x  # Python НЕ знает заранее, что x=10
        # Это ищется в runtime в globals/locals

# Динамические типы
def add(a, b):
    return a + b  # BINARY_ADD работает для int, str, list и т.д.
                   # PVM проверяет тип в runtime

# Динамические атрибуты
obj.attr  # Атрибут может быть добавлен в runtime

Это причина почему Python медленнее статически типизированных языков.

8. Как оптимизировать компиляцию и выполнение

1. Используйте Cython (для критических частей)

# fib.pyx
def fib_cython(int n):
    if n <= 1:
        return n
    return fib_cython(n-1) + fib_cython(n-2)
# Компилируем в C
cython fib.pyx
gcc -c fib.c -I/usr/include/python3.10
gcc -shared fib.o -o fib.so

Результат: 100x+ ускорение по сравнению с Python.

2. Используйте NumPy для векторизации

# Медленно (Python loop)
result = []
for i in range(1000000):
    result.append(arr[i] * 2)

# Быстро (NumPy, выполняется на C)
result = arr * 2

3. Используйте multiprocessing для CPU-bound задач

from multiprocessing import Pool

def cpu_bound_task(x):
    return sum(range(x))

with Pool(4) as pool:
    results = pool.map(cpu_bound_task, range(1000000))

9. Инструменты для анализа bytecode

dis модуль:

import dis

def example():
    x = 1
    y = x + 2
    return y

dis.dis(example)

marshal для сохранения/загрузки:

import marshal

code = compile('print("Hello")', '<string>', 'exec')
marshal.dump(code, open('code.pyc', 'wb'))

compile() для динамической компиляции:

code_string = 'x = 1; y = x + 2'
compiled = compile(code_string, '<string>', 'exec')
exec(compiled)

Вывод

Ключевые точки о компиляции в Python:

  1. Python НЕ интерпретирует исходный код напрямую — компилирует в bytecode
  2. Bytecode сохраняется в .pyc файлы для ускорения повторных импортов
  3. PVM интерпретирует bytecode инструкции (медленнее, чем машинный код)
  4. Для оптимизации используйте: NumPy, Cython, PyPy, Numba
  5. Python динамичен, поэтому много решений принимается в runtime

Это объясняет, почему Python медленнее C, но гораздо удобнее для разработки — компромисс между скоростью и гибкостью.

Что знаешь про компиляцию в Python? | PrepBro