Способы компиляции кода на Python: обзор

Раздел: Теория программирования -> Характеристики языка

Компиляция в Python: особенности и подходы

Python традиционно считается интерпретируемым языком, однако его работа включает этап компиляции в промежуточный байт-код, а также существуют технологии, позволяющие получить настоящий машинный код. В статье рассматриваются различные способы компиляции кода на Python, их цели, примеры и типичные сложности.

Как Python совмещает интерпретацию и компиляцию?

Стандартная реализация CPython при запуске скрипта сначала компилирует исходный код (.py) в байт-код (.pyc), который затем выполняется виртуальной машиной. Этот процесс происходит автоматически и незаметно для разработчика.

# simple.py
print("Привет, мир!")

a = 10
b = 20
print(a + b)

Python компилируемый язык программирования (python как компилируемый язык)

При первом запуске создаётся папка __pycache__ с файлом simple.cpython-3XX.pyc. Байт-код можно посмотреть с помощью модуля dis:

python -m dis simple.py
  1           0 LOAD_GLOBAL              0 (print)
              2 LOAD_CONST               1 ('Привет, мир!')
              4 CALL_FUNCTION            1
              6 POP_TOP
  3           8 LOAD_CONST               2 (10)
             10 STORE_FAST               0 (a)
  4          12 LOAD_CONST               3 (20)
             14 STORE_FAST               1 (b)
  5          16 LOAD_GLOBAL              0 (print)
             18 LOAD_FAST                0 (a)
             20 LOAD_FAST                1 (b)
             22 BINARY_ADD
             24 CALL_FUNCTION            1
             26 POP_TOP
             28 LOAD_CONST               0 (None)
             30 RETURN_VALUE

Байт-код – это инструкции для стековой виртуальной машины, а не машинный код. Поэтому CPython остаётся интерпретатором, хотя и с компиляцией в промежуточное представление.

Типичная проблема:

Начинающие разработчики полагают, что .pyc файлы ускоряют выполнение. На самом деле они ускоряют только загрузку модуля (подготовку), сам код выполняется с той же скоростью, что и без кеша. Кроме того, байт-код не является машинным и не даёт преимуществ производительности по сравнению с интерпретацией строк.

Решение:

Для повышения производительности следует использовать другие подходы: JIT-компиляцию (PyPy), компиляцию в C (Cython) или специализированные библиотеки (Numba).

Как добиться производительности машинного кода в Python с помощью PyPy?

PyPy – это альтернативный интерпретатор Python, который использует JIT-компиляцию (Just-In-Time). Он анализирует часто выполняемые участки кода и компилирует их в машинный код на лету.

# test_loop.py
def compute():
    total = 0
    for i in range(10_000_000):
        total += i
    return total

if __name__ == "__main__":
    print(compute())

Сравнение скорости CPython и PyPy:

time python test_loop.py   # CPython
time pypy3 test_loop.py    # PyPy
# Примерные результаты (реальные значения зависят от системы)
CPython: 0.45s user
PyPy: 0.09s user

Возникающие проблемы:

PyPy не полностью совместим с C-расширениями, написанными через CPython API (например, numpy, pandas могут работать медленнее или требовать специальных версий). Также PyPy потребляет больше памяти из-за JIT-компилятора.

Решение:

Для проектов, сильно зависимых от C-расширений, лучше использовать CPython. PyPy подходит для чистого Python-кода с интенсивными вычислениями (циклы, рекурсия).

Как превратить Python-код в C-расширение с помощью Cython?

Cython позволяет писать код на расширенном синтаксисе Python (файлы .pyx), который затем транслируется в C и компилируется в нативный модуль. Можно использовать статические типы для ускорения.

# example.pyx
def sum_cy(int n):
    cdef int i
    cdef long total = 0
    for i in range(n):
        total += i
    return total

Компиляция через setup.py:

from setuptools import setup
from Cython.Build import cythonize

setup(
    ext_modules=cythonize("example.pyx")
)

# Выполнить:
# python setup.py build_ext --inplace

Импорт и использование:

import example
print(example.sum_cy(10_000_000))

Типичные ошибки:

Необходимо устанавливать Cython и компилятор C (например, Visual Studio на Windows, gcc на Linux). Синтаксис .pyx отличается от обычного Python (cdef, cpdef). Ошибки типизации могут привести к непредсказуемым результатам.

Решение:

Начинать с минимальных изменений, постепенно добавляя объявления типов. Использовать дистрибутив Anaconda, где многие зависимости предустановлены.

Как ускорить математические функции без переписывания на C с помощью Numba?

Numba – это JIT-компилятор для числового Python (numpy, math). Достаточно добавить декоратор @jit к функции, и Numba компилирует её в машинный код при первом вызове.

import numba
import time

@numba.jit
def sum_numba(n):
    total = 0
    for i in range(n):
        total += i
    return total

start = time.time()
print(sum_numba(10_000_000))
print("Time:", time.time() - start)

Без Numba та же функция будет работать в разы медленнее.

Проблемы и ограничения:

Numba лучше всего работает с циклами и операциями numpy, но не поддерживает все возможности Python (например, динамические типы, исключения, объекты). При неподходящем коде компиляция может отказать или выдать ошибку.

Решение:

Использовать режим nopython=True для строгой компиляции. Перед запуском проверить, какие операции поддерживаются в документации Numba.

Как получить нативный код из Python с аннотациями типов с помощью Mypyc?

Mypyc – это компилятор, который преобразует статически типизированный Python-код (с аннотациями) в C-расширение, аналогично Cython, но используя стандартный синтаксис mypy.

# example_mypy.py
def sum_mypy(n: int) -> int:
    total: int = 0
    for i in range(n):
        total += i
    return total

Компиляция:

pip install mypy mypyc
mypyc example_mypy.py
# создаётся example_mypy.cpython-3XX-x86_64-linux-gnu.so

Использование скомпилированного модуля:

import example_mypy
print(example_mypy.sum_mypy(10_000_000))

Сложности:

Требуется полная статическая типизация всех переменных и возвращаемых значений. Динамические черты (например, смешивание типов в списке) не поддерживаются. Ошибки типизации mypy приводят к ошибкам компиляции.

Решение:

Постепенно добавлять типы, использую mypy как линтер. Для сложных проектов сначала проверять типы, а затем компилировать отдельные модули.

Расширенные примеры компиляции кода на Python

1. Анализ байт-кода с помощью модуля dis

Пример
import dis

def factorial(n):
    if n == 0:
        return 1
    else:
        return n * factorial(n - 1)

dis.dis(factorial)
  2           0 LOAD_FAST                0 (n)
              2 LOAD_CONST               1 (0)
              4 COMPARE_OP               2 (==)
              6 POP_JUMP_IF_FALSE       12

  3           8 LOAD_CONST               2 (1)
             10 RETURN_VALUE

  5     >>   12 LOAD_FAST                0 (n)
             14 LOAD_GLOBAL              0 (factorial)
             16 LOAD_FAST                0 (n)
             18 LOAD_CONST               2 (1)
             20 BINARY_SUBTRACT
             22 CALL_FUNCTION            1
             24 BINARY_MULTIPLY
             26 RETURN_VALUE

Показан байт-код рекурсивной функции. Каждая инструкция выполняется виртуальной машиной CPython. Это не машинный код, но промежуточное представление, полученное в результате компиляции.

2. Сравнение производительности CPython и PyPy на числовом цикле

Пример
import time

def heavy_calc(limit):
    result = 0
    for i in range(limit):
        result += i ** 2 - i * 3
    return result

start = time.perf_counter()
heavy_calc(50_000_000)
print("CPython:", time.perf_counter() - start)

# Запуск на PyPy даёт значительно меньшее время
# Вывод в CPython (пример): 5.23 сек
# Вывод в PyPy (пример): 0.87 сек

PyPy благодаря JIT-компиляции ускоряет подобные вычислительные задачи в 5-10 раз.

3. Cython: компиляция с явным объявлением типов

Пример
# sum_cy.pyx
def sum_range(int n):
    cdef int i
    cdef long long total = 0
    for i in range(n):
        total += i
    return total
Пример
# setup.py
from setuptools import setup
from Cython.Build import cythonize

setup(
    ext_modules=cythonize("sum_cy.pyx")
)

# Команда: python setup.py build_ext --inplace
Пример
# Использование в Python
import sum_cy
print(sum_cy.sum_range(100_000_000))
4999999950000000

Результат возвращается быстро, так как функция выполняется как скомпилированный C-код.

4. Numba: JIT для функций с numpy

Пример
import numba
import numpy as np
import time

@numba.jit(nopython=True)
def dot_product(a, b):
    result = 0.0
    for i in range(a.shape[0]):
        result += a[i] * b[i]
    return result

x = np.random.rand(10_000_000)
y = np.random.rand(10_000_000)

start = time.perf_counter()
res = dot_product(x, y)
print("Numba JIT:", time.perf_counter() - start)
Numba JIT: 0.038 сек

Без Numba цикл по numpy массиву в Python занимал бы несколько секунд. Декоратор @jit компилирует функцию в машинный код при первом вызове.

5. Mypyc: компиляция статически типизированного модуля

Пример
# calc.py
from typing import List

def sum_even(numbers: List[int]) -> int:
    total: int = 0
    for num in numbers:
        if num % 2 == 0:
            total += num
    return total
Пример
# Компиляция в терминале:
# mypyc calc.py
# Или через setup.py аналогично Cython
Пример
import calc
data = list(range(1_000_000))
print(calc.sum_even(data))
250000500000

Скомпилированный модуль работает быстрее оригинального Python-кода за счёт нативного выполнения, при этом сохраняется совместимость с синтаксисом Python.

Python как компилируемый язык - comments

En
Python компилируемый язык программирования (python)