Расширения на C для Python: когда оправдано и что учесть
Перед тем как переписывать горячий код на C, стоит понять, где реально узкое место и во что обойдётся сопровождение биндинга. Разбираем критерии выбора и цену каждого подхода.

Профилировщик показывает, что 80% времени функция проводит в цикле по массиву чисел на чистом Python. Соблазн переписать её на C очевиден: горячий цикл в компилируемом коде часто быстрее интерпретируемого в десятки раз. Но C-расширение — это не только скорость, а ещё сборка под каждую платформу, ручное управление подсчётом ссылок и падение всего процесса при разыменовании битого указателя. Разберём, когда переход на C оправдан, какие есть альтернативы и чем платишь за каждую.
Когда C вообще имеет смысл
Питоновский интерпретатор (CPython 3.12) тратит время на диспетчеризацию байткода, упаковку и распаковку объектов, проверку типов на каждой операции. C-код от этого свободен: он работает с сырыми double и long напрямую. Выигрыш появляется там, где этих операций много и они простые.
Прежде чем писать хоть строчку на C, ответьте на три вопроса:
- Профилировали ли вы? Запустите
cProfileилиpy-spy. Если функция занимает 3% общего времени, ускорение её в 50 раз даст выигрыш в пределах погрешности. - Это CPU-bound или IO-bound? Если код ждёт сеть или диск, C не поможет — узкое место не в вычислениях.
- Нельзя ли обойтись готовой библиотекой? NumPy, Polars, orjson уже написаны на C или Rust. Ваш велосипед вряд ли будет быстрее и точно будет менее протестирован.
Расширение на C оправдано, когда узкое место — плотный числовой или байтовый цикл, который нельзя векторизовать через NumPy, и это подтверждено профилировщиком. Во всех остальных случаях сначала ищите библиотеку.
Четыре пути ускорить горячий код
Написание расширения на голом C Python API — крайний вариант. Между чистым Python и ним есть промежуточные ступени, у каждой своя цена.
1. Векторизация через NumPy
Если данные — это массивы чисел, переписывание цикла в векторные операции NumPy часто закрывает вопрос без единой строки C. Цикл на миллион элементов, свёрнутый в numpy.sum(arr ** 2), уходит в скомпилированный код библиотеки. Цена: нужно уметь думать массивами, и не всякий алгоритм ложится на векторные операции.
2. Cython
Cython (0.29.x / 3.x) — надстройка над Python с типизированными аннотациями. Вы пишете почти обычный Python, добавляете cdef int i, и компилятор генерирует C-код, который собирается в модуль. Порог входа ниже, чем у чистого C API, а управление ссылками Cython берёт на себя.
# файл fib.pyx, Cython 3.x
# аннотации типов превращают Python-цикл в машинный код
def fib(int n):
cdef long a = 0
cdef long b = 1
cdef int i
for i in range(n):
a, b = b, a + b
return a
Цена: отдельный шаг компиляции, файл .pyx и конфигурация сборки. Отлаживать сгенерированный C неприятно.
3. Расширение на голом C API
Полный контроль и максимальная скорость, но и максимальная ответственность. Вы вручную конвертируете аргументы через PyArg_ParseTuple, следите за подсчётом ссылок (Py_INCREF / Py_DECREF), возвращаете PyObject*. Ошибка в счётчике ссылок — либо утечка памяти, либо преждевременное освобождение и падение процесса.
// mymod.c — минимальное расширение под CPython 3.12
#define PY_SSIZE_T_CLEAN
#include <Python.h>
// возводит целое число в квадрат
static PyObject *square(PyObject *self, PyObject *args) {
long value;
if (!PyArg_ParseTuple(args, "l", &value)) {
return NULL; // исключение уже установлено парсером
}
return PyLong_FromLong(value * value);
}
static PyMethodDef Methods[] = {
{"square", square, METH_VARARGS, "Возвести число в квадрат"},
{NULL, NULL, 0, NULL}
};
static struct PyModuleDef moduledef = {
PyModuleDef_HEAD_INIT, "mymod", NULL, -1, Methods
};
PyMODINIT_FUNC PyInit_mymod(void) {
return PyModule_Create(&moduledef);
}
Собирается через setuptools. Минимальный pyproject.toml плюс setup.py с Extension("mymod", ["mymod.c"]) — точный синтаксис секции ext_modules сверяйте в документации setuptools вашей версии, он менялся.
4. ctypes / cffi над готовой .so
Если C-код у вас уже есть в виде библиотеки, необязательно писать биндинг. ctypes из стандартной библиотеки или cffi вызывают функции из .so/.dll напрямую из Python. Цена: оверхед на каждый вызов, поэтому подходит для нечастых вызовов тяжёлых функций, а не для миллиона мелких.
Сравнение подходов
| Подход | Порог входа | Управление памятью | Когда выбирать |
|---|---|---|---|
| NumPy | Низкий | Автоматом | Данные — массивы чисел, алгоритм векторизуется |
| Cython | Средний | В основном автоматом | Циклы, не ложащиеся на NumPy; хочется остаться близко к Python |
| C API | Высокий | Вручную, INCREF/DECREF | Нужен максимум скорости и полный контроль, есть кому сопровождать |
| ctypes / cffi | Низкий-средний | Частично вручную | Готовая C-библиотека, редкие тяжёлые вызовы |
GIL: главный подводный камень
Пока ваш C-код выполняется, он по умолчанию держит GIL* — глобальную блокировку интерпретатора. Это значит, что другие Python-потоки простаивают. Если цель расширения — распараллелить вычисления по потокам, GIL нужно явно отпустить на время чистого C-счёта макросами Py_BEGIN_ALLOW_THREADS / Py_END_ALLOW_THREADS. Но внутри освобождённого блока запрещено трогать любые PyObject — только сырые C-данные. Забыли это правило — получили гонку и падение под нагрузкой, которое не воспроизводится в один поток.
* GIL (Global Interpreter Lock) — блокировка, позволяющая в каждый момент выполнять байткод CPython только одному потоку. В CPython 3.13 появилась экспериментальная сборка без GIL, но это отдельная история и на неё пока нельзя полагаться в проде.
Что ещё платите за C-расширение
- Сборка под каждую платформу. Пользователю нужен компилятор, либо вы собираете колёса (wheels) под Linux, macOS, Windows и разные версии Python. Для этого обычно берут
cibuildwheelв CI. - Отладка. Сегфолт в C уронит весь процесс без Python-трейсбека. Отлаживать придётся через gdb/lldb, а не через
pdb. - Совместимость версий. C API меняется между минорными версиями CPython. Модуль, собранный под 3.11, не загрузится в 3.12 без пересборки. Стабильный ABI (
Py_LIMITED_API) снимает часть проблемы, но ограничивает набор функций. - Обработка ошибок. Каждую функцию Python API, которая может вернуть NULL, нужно проверять и корректно пробрасывать исключение. Пропущенная проверка — тихая порча состояния интерпретатора.
Как измерить, что стало лучше
Не доверяйте ощущению «стало быстрее». Замеряйте на реальных данных и в тех же условиях:
- Зафиксируйте базовую версию:
timeitна репрезентативном входе, тот же объём данных и то же железо. - Профилируйте до оптимизации — убедитесь, что цель действительно горячая.
- После переписывания замерьте ту же нагрузку. Ускорение функции в 40 раз при её доле в 20% общего времени даёт итоговый выигрыш около 19% — этого может не стоить обвязка на C.
- Проверьте память: C-код с ошибкой в счётчике ссылок течёт медленно и заметен только под долгой нагрузкой. Прогоните под
valgrindили следите за RSS процесса в течение часа.
Что делать дальше
Начните с профилировщика и NumPy — в большинстве задач до C дело не дойдёт. Если цикл не векторизуется, попробуйте Cython прежде, чем браться за голый API: он даёт 80% выигрыша при доле усилий в разы меньшей. К чистому C API переходите, только когда Cython упирается в потолок и есть кому поддерживать биндинг годами. И заранее заложите в проект CI со сборкой колёс под все нужные платформы — иначе расширение станет работать только на вашей машине.
Автоэксперт / механик: Расшифровка OBD2-кодов, выбор б/у авто, диагностика по симптомам.
Спросить за 12 ₽Источники: Extending Python with C or C++ — Python 3 docs, Cython documentation
Частые вопросы
А если у меня PyPy, а не CPython?
Расширение собралось, но падает с Segmentation fault — с чего начать?
Стоит ли переписывать на C, если функция даёт 5% общего времени?
Модуль собран под Python 3.11, а на сервере 3.12 — почему не грузится?
Можно ли ускорить код без C, оставаясь на чистом Python?
Cython или голый C API — что выбрать новичку в расширениях?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.