Расширение возможностей Python внешними C модулями
Эффективное решение: использование Cython
Как написать высокопроизводительное расширение на C, сохраняя простоту Python?
Cython позволяет создавать компилируемые модули, которые напрямую вызывают код на C/C++. Он транслирует код на Python-подобном языке в C, после чего компилируется в разделяемую библиотеку (.pyd или .so).
# example.pyx
cdef int add_c(int a, int b):
return a + b
def add(int a, int b):
return add_c(a, b)C python lib (библиотеки c для python)
Файл setup.py для сборки:
from setuptools import setup
from Cython.Build import cythonize
setup(
ext_modules = cythonize("example.pyx")
)
Сборка выполняется командой python setup.py build_ext --inplace. После этого модуль можно импортировать как обычный Python-модуль. Cython автоматически управляет типами, памятью и обработкой исключений.
Возможные проблемы:
- Ошибки компиляции из-за несоответствия типов C. Решение: явно указывать типы с помощью
cdef. - Утечки памяти при работе с динамическими структурами. Использовать
cython.viewили RAII-обёртки. - Проблемы с совместимостью версий Python и Cython. Проверять таблицу совместимости на сайте Cython.
Случаи использования:
- Ускорение узких мест в численных расчётах (numpy, pandas).
- Интеграция с существующими библиотеками на C (например, libpng, libxml).
- Создание обёрток для C++ через
cythonс использованиемcimport.
Как вызвать функции из динамической библиотеки без компиляции?
Решение ctypes - часть стандартной библиотеки Python. Позволяет загружать разделяемые библиотеки (.so, .dll) и вызывать C-функции, описывая их сигнатуры.
import ctypes
lib = ctypes.CDLL("./mylib.so")
lib.add.argtypes = [ctypes.c_int, ctypes.c_int]
lib.add.restype = ctypes.c_int
result = lib.add(5, 3)
print(result) # 8
Типичные ошибки:
- Забытые
argtypesиrestypeприводят к неопределённому поведению и падениям. Всегда указывать прототипы. - Неправильное преобразование строк: ctypes использует
c_char_p, но строки Python нужно кодировать в байты. - Библиотека не найдена - проверять пути и переменную
LD_LIBRARY_PATH(Linux) илиPATH(Windows).
Случаи использования:
- Быстрый прототип взаимодействия с любой C-библиотекой (без настройки сборки).
- Одноразовые скрипты и тесты.
Как безопасно вызывать C-код с автоматической генерацией обвязки?
CFFI (C Foreign Function Interface) предлагает два режима: ABI (in-line) и API (out-of-line). Режим API компилирует C-заглушки, что даёт лучшую производительность и меньше ошибок.
# example_build.py
from cffi import FFI
ffi = FFI()
ffi.cdef("""
int add(int a, int b);
""")
ffi.set_source("_example",
"""
int add(int a, int b) {
return a + b;
}
""")
ffi.compile()
# после компиляции импортируем
from _example import ffi, lib
print(lib.add(2, 3)) # 5
Проблемы:
- Необходимость компилятора C на целевой машине (для режима API).
- Сложность отладки сегфолтов: в CFFI ошибки маскируются в объект
ffi.error. - Управление памятью для структур требует явных вызовов
ffi.newиffi.gc.
Как подключить C++ библиотеку с минимальными усилиями?
PyBind11 - современная библиотека для создания расширений Python из C++11. Требует компиляции, но даёт полный контроль над трансформацией типов.
#include <pybind11/pybind11.h>
namespace py = pybind11;
int add(int a, int b) {
return a + b;
}
PYBIND11_MODULE(example, m) {
m.def("add", &add, "A function that adds two numbers");
}
Сборка через CMake или pybind11/setup_helpers.py. Результат:
>>> import example >>> example.add(2, 3) 5
Сложности:
- Зависимость от C++ компилятора и стандартной библиотеки.
- Проблемы с перегрузкой функций и аргументами по умолчанию - требуется явная регистрация.
- Обработка исключений C++ в Python - использовать
py::exception.
Случаи использования:
- Существующий C++ код, который нужно экспортировать в Python.
- Проекты, где важна производительность и удобство написания расширений на C++.
Расширенные примеры интеграции C и Python
1. Работа с C-структурами через ctypes
// point.h
typedef struct {
double x;
double y;
} Point;
double distance(Point *a, Point *b);
// point.c
#include <math.h>
#include "point.h"
double distance(Point *a, Point *b) {
double dx = a->x - b->x;
double dy = a->y - b->y;
return sqrt(dx*dx + dy*dy);
}
Компиляция библиотеки: gcc -shared -o libpoint.so -fPIC point.c -lm
# Python код
import ctypes
lib = ctypes.CDLL("./libpoint.so")
class Point(ctypes.Structure):
_fields_ = [("x", ctypes.c_double),
("y", ctypes.c_double)]
lib.distance.argtypes = [ctypes.POINTER(Point), ctypes.POINTER(Point)]
lib.distance.restype = ctypes.c_double
a = Point(0.0, 0.0)
b = Point(3.0, 4.0)
result = lib.distance(ctypes.byref(a), ctypes.byref(b))
print(f"Расстояние: {result}") # 5.0
Расстояние: 5.0
Пояснение:
Структура Point объявлена в Python как класс, наследующий ctypes.Structure. Поле _fields_ отображает имена и типы C. Для передачи указателей используется ctypes.byref(). Важно следить за выравниванием: если структура содержит #pragma pack, нужно задать _pack_.
2. Манипуляция массивами в CFFI с возвратом большого объёма данных
# build.py
from cffi import FFI
ffi = FFI()
ffi.cdef("""
int* generate_array(int size);
void free_array(int* arr);
""")
ffi.set_source("_array_example",
"""
#include <stdlib.h>
int* generate_array(int size) {
int* arr = (int*)malloc(size * sizeof(int));
for(int i=0; i<size; i++) arr[i] = i*i;
return arr;
}
void free_array(int* arr) { free(arr); }
""")
ffi.compile()
# usage.py
from _array_example import ffi, lib
size = 10
ptr = lib.generate_array(size)
arr = ffi.unpack(ptr, size) # преобразует в список Python
print(arr[:5])
lib.free_array(ptr) # обязательно освободить память
[0, 1, 4, 9, 16]
Функция ffi.unpack создаёт временный список. Для работы с огромными массивами удобнее создать ffi.buffer или обертку в numpy с помощью ffi.from_buffer.
3. Использование Cython для вызова C-функции из библиотеки OpenSSL (хеширование)
# hash.pyx
from libc.stdlib cimport malloc, free
from libc.string cimport memcpy
cdef extern from "openssl/md5.h":
int MD5(const unsigned char *d, unsigned long n, unsigned char *md)
def md5_hash(bytes data):
cdef unsigned char[16] result
cdef unsigned char *buf = data
MD5(buf, len(data), result)
return bytes(result[:16])
# setup.py
from setuptools import setup
from Cython.Build import cythonize
setup(
ext_modules = cythonize("hash.pyx"),
libraries = ["crypto"]
)
>>> import hash >>> hash.md5_hash(b"Hello") b'\x8b\x1a...'
В Cython используется cdef extern для объявления C-функции. Ссылка на библиотеку libcrypto добавляется в setup.py через libraries. Память под результат выделяется на стеке (массив фиксированного размера), что ускоряет выполнение.
4. Передача строк и Callback-функций через PyBind11
# callback_example.cpp
#include <pybind11/pybind11.h>
#include <pybind11/functional.h>
#include <string>
namespace py = pybind11;
void repeat_twice(const std::string &msg, py::function callback) {
std::string result = msg + " " + msg;
callback(result);
}
PYBIND11_MODULE(callback_mod, m) {
m.def("repeat_twice", &repeat_twice, "Calls callback with repeated string");
}
# Python
import callback_mod
def my_print(s):
print("Из C++:", s)
callback_mod.repeat_twice("Привет", my_print)
Из C++: Привет Привет
PyBind11 автоматически преобразует std::string в Python str и py::function в вызываемый объект. Включение заголовка pybind11/functional.h обязательно для поддержки колбэков. Производительность выше, чем через ctypes, но требует компиляции.
5. Прямая работа с Python C API (ручное расширение) – пример модуля со счётчиком
// counter.c
#include <Python.h>
static PyObject* increment(PyObject *self, PyObject *args) {
int val;
if (!PyArg_ParseTuple(args, "i", &val))
return NULL;
return PyLong_FromLong(val + 1);
}
static PyMethodDef CounterMethods[] = {
{"increment", increment, METH_VARARGS, "Increment an integer"},
{NULL, NULL, 0, NULL}
};
static struct PyModuleDef countermodule = {
PyModuleDef_HEAD_INIT,
"counter",
NULL,
-1,
CounterMethods
};
PyMODINIT_FUNC PyInit_counter(void) {
return PyModule_Create(&countermodule);
}
# setup.py
from setuptools import setup, Extension
module = Extension('counter', sources=['counter.c'])
setup(name='counter', ext_modules=[module])
>>> import counter >>> counter.increment(5) 6
Это «классический» способ, требующий ручного управления ссылками и подсчётом объектов. Ошибка в коде C может привести к падению интерпретатора. Используется, когда необходима максимальная интеграция с Python (например, создание собственных типов).