Давайте поговорим о слоне в комнате: Python медленный. Я это сказал. Прежде чем энтузиасты Python возьмут в руки вилы, позвольте мне пояснить: Python не медленный из-за плохого дизайна. Он медленный, потому что ставит счастье разработчика выше сырой скорости. И честно говоря, обычно это нормально. До тех пор, пока не становится плохо. Когда ваше приложение начинает задыхаться от вычислительных задач, когда вложенные циклы становятся чёрными дырами производительности, когда ваши пользователи начинают сомневаться в своём выборе, ожидая завершения скрипта — вот тогда вам нужен Cython. Думайте о Cython как о спортивном кузене Python, который ходил в спортзал и выучил C. То же дружелюбное лицо, но теперь он может поднять производительность ваших узких мест.
Проблема скорости, о которой никто не хочет говорить
Интерпретируемая природа Python — это и благословение, и проклятие. Каждая строка кода переводится «на лету» интерпретатором, что добавляет накладные расходы — и немалые. Когда вы пишете простой цикл в Python, интерпретатор должен проверять типы, управлять памятью, обрабатывать исключения и выполнять дюжину других служебных задач для каждой итерации. Это как иметь личного помощника, который настаивает на том, чтобы спрашивать разрешения перед каждым действием. Полезно, но утомительно.
Рассмотрим эту безобидную на вид функцию, которая вычисляет сумму квадратов:
def sum_of_squares(numbers):
return sum(x * x for x in numbers)
Элегантно, читабельно, в духе Python. Но когда вы передадите ей миллион чисел, у вас будет время сварить кофе, проверить электронную почту и задуматься о смысле жизни, прежде чем она завершит работу.
Enter Cython: Умножитель производительности
Cython устраняет разрыв между простотой использования Python и сырой производительностью C. Это надмножество Python, то есть допустимый код Python является допустимым кодом Cython. Но здесь интересно следующее: Cython компилирует ваш код в C, который затем компилируется в машинный код. Результат? Повышение производительности в диапазоне от «хорошего» до «я только что нарушил законы физики».
Прелесть Cython заключается в стратегии постепенного внедрения. Вам не нужно переписывать всё приложение. Вы выявляете узкие места, оптимизируете эти конкретные функции и оставляете всё остальное как чистый Python. Это оптимизация с хирургической точностью.
Настройка вашего поля боя Cython
Прежде чем мы начнём оптимизацию, нам нужно настроить среду разработки. Не волнуйтесь, это менее болезненно, чем настройка webpack (извините, разработчики JavaScript).
Сначала установите Cython:
pip install cython
Вам также понадобится C-компилятор. В Linux у вас, вероятно, уже есть GCC. На macOS установите инструменты командной строки Xcode. В Windows установите Microsoft Visual C++. Да, пользователи Windows, я вижу вашу гримасу. Оно того стоит, поверьте мне.
Теперь создайте базовую структуру проекта:
my_project/
├── setup.py
├── main.py
└── optimized.pyx
Расширение .pyx
— это место, где происходит волшебство Cython. Здесь мы будем писать наш оптимизированный код.
Ваша первая оптимизация Cython
Давайте начнём с чего-то простого, но поучительного. Вот функция Python, которая вычисляет числа Фибоначчи рекурсивно:
def fibonacci_py(n):
if n <= 1:
return n
return fibonacci_py(n - 1) + fibonacci_py(n - 2)
Теперь создадим optimized.pyx
с версией Cython:
def fibonacci_cy(int n):
if n <= 1:
return n
return fibonacci_cy(n - 1) + fibonacci_cy(n - 2)
Видите разницу? Всего одно слово: int
. Эта единственная аннотация типа говорит Cython обрабатывать n
как целое число C, а не как объект Python. Это устраняет накладные расходы на проверку типа и позволяет выполнять арифметические операции на уровне C.
Создайте файл setup.py
для компиляции вашего кода Cython:
from setuptools import setup
from Cython.Build import cythonize
setup(
ext_modules=cythonize("optimized.pyx")
)
Компилируйте его:
python setup.py build_ext --inplace
Теперь вы можете импортировать и использовать вашу оптимизированную функцию:
from optimized import fibonacci_cy
result = fibonacci_cy(30)
print(f"Результат: {result}")
Аннотации типов: Секретный ингредиент
Аннотации типов — это то, что превращает Cython из «слегка более быстрого Python» в «сверхобычный быстрый код, похожий на C». Когда вы объявляете типы, вы даёте компилятору разрешение пропустить динамическую систему типов Python и использовать эффективные операции C.
Вот практический пример с операциями над массивами:
def sum_of_squares_cy(double[:] numbers):
cdef int i
cdef double result = 0
for i in range(len(numbers)):
result += numbers[i] * numbers[i]
return result
Давайте разберёмся, что происходит:
- Просмотры памяти (
double[:]
) предоставляют облегчённый интерфейс для доступа к данным массива без накладных расходов на объекты Python. Они быстрее списков и даже быстрее массивов NumPy для поэлементных операций. - Объявления cdef (
cdef int i
) создают чистые переменные C. Они не являются объектами Python — они выделяются в стеке, проверяются по типу во время компиляции и работают быстро. - Циклы на уровне C выполняются без накладных расходов интерпретатора Python. Каждая итерация — это чистый машинный код.
Пример из реального мира: Расчёт расстояний между матрицами
Давайте займёмся чем-то более реалистичным. Предположим, вы создаёте систему рекомендаций, которая должна вычислять расстояния между тысячами высокоразмерных векторов. Вот версия чистого Python:
import numpy as np
def compute_distances_py(X, Y):
m, n = len(X), len(Y)
distances = np.zeros((m, n))
for i in range(m):
for j in range(n):
diff = X[i] - Y[j]
distances[i, j] = np.sqrt(np.sum(diff * diff))
return distances
Это работает, но вложенные циклы убивают производительность. Каждая итерация включает вызовы функций Python, операции с массивами NumPy и динамическую проверку типов.
Теперь версия Cython (distances.pyx
):
import numpy as np
cimport numpy as cnp
from libc.math cimport sqrt
def compute_distances_cy(double[:, :] X, double[:, :] Y):
cdef int m = X.shape
cdef int n = Y.shape
cdef int d = X.shape
cdef int i, j, k
cdef double diff, dist
cdef cnp.ndarray[cnp.float64_t, ndim=2] distances = np.zeros((m, n))
for i in range(m):
for j in range(n):
dist = 0.0
for k in range(d):
diff = X[i, k] - Y[j, k]
dist += diff * diff
distances[i, j] = sqrt(dist)
return distances
Ключевые оптимизации:
cimport
импортирует определения NumPy на уровне C- Просмотры памяти для входных массивов устраняют накладные расходы на объекты Python
- Прямой доступ к функции
sqrt
из libc вместо модуля math Python - Все переменные циклов — целые числа C
- Промежуточные вычисления используют числа с плавающей запятой C
Обновите setup.py
:
from setuptools import setup, Extension
from Cython.Build import cythonize
import numpy
extensions = [
Extension(
"distances",
["distances.pyx"],
include_dirs=[numpy.get_include()]
)
]
setup(
ext_modules=cythonize(extensions, compiler_directives={'language_level': "3"})
)
Беничмаркинг: Доказательство в производительности
Давайте сравним производительность с помощью надлежащего бенчмарка:
import time
import numpy as np
from distances import compute_distances_cy
def compute_distances_py(X, Y):
m, n = len(X), len(Y)