Способы компиляции кода на 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.