Расширение возможностей Python через модули на C++
Основные подходы к созданию C++ модулей для Python
Как создать эффективный и удобный модуль на C++ для Python с минимальными усилиями и современным синтаксисом?
Наиболее эффективным решением на сегодняшний день является использование библиотеки pybind11. Она позволяет автоматически генерировать привязки между C++ и Python, поддерживает классы, перегрузки, исключения, итераторы и даже работу с NumPy. Для работы потребуется компилятор C++14 или новее.
// example.cpp
#include <pybind11/pybind11.h>
namespace py = pybind11;
int add(int i, int j) {
return i + j;
}
struct Pet {
Pet(const std::string &name) : name(name) {}
void setName(const std::string &name_) { name = name_; }
const std::string &getName() const { return name; }
std::string name;
};
PYBIND11_MODULE(example, m) {
m.doc() = "pybind11 example module";
m.def("add", &add, "A function that adds two numbers");
py::class_<Pet>(m, "Pet")
.def(py::init<const std::string &>())
.def("setName", &Pet::setName)
.def("getName", &Pet::getName)
.def_readwrite("name", &Pet::name);
}Python module attributes (атрибуты модуля в python)
# setup.py
import pybind11
from setuptools import setup, Extension
ext_module = Extension(
'example',
sources=['example.cpp'],
include_dirs=[pybind11.get_include()],
language='c++'
)
setup(
name='example',
version='0.1',
ext_modules=[ext_module],
install_requires=['pybind11']
)Python module version (версия модуля python)
Сборка выполняется командой python setup.py build_ext --inplace. После сборки модуль можно импортировать:
import example
print(example.add(3, 4)) # 7
pet = example.Pet("Rex")
print(pet.getName()) # RexPython cpp module (взаимодействие python с модулями c++)
Возможные проблемы:
- Ошибка компиляции из-за отсутствия pybind11: установите через
pip install pybind11. - Проблемы с ABI на разных версиях Python: используйте ту же версию Python и компилятора, что и при сборке.
- На Windows может потребоваться указать путь к Visual Studio или установить MinGW.
Как вызвать уже существующую C++ библиотеку, не переписывая код?
Используйте ctypes - встроенный модуль Python. Для этого функции C++ должны быть объявлены с extern "C", чтобы избежать name mangling. Пример:
// math_ops.cpp (компилируется в .so/.dll)
extern "C" {
int multiply(int a, int b) { return a * b; }
double divide(double a, double b) { return a / b; }
}Python module cv2 (модуль cv2 (opencv) в python)
# call_math.py
import ctypes
lib = ctypes.CDLL('./math_ops.so') # Linux/Mac
lib.multiply.restype = ctypes.c_int
lib.multiply.argtypes = [ctypes.c_int, ctypes.c_int]
print(lib.multiply(6, 7)) # 42Python encodings module (модуль encodings в python)
Типичные ошибки:
- Загрузка .so с неверным путем: укажите абсолютный или используйте
os.path.dirname(__file__). - Несоответствие типов: задавайте
restypeиargtypesявно. - Проблемы с передачей структур: необходимо вручную описывать
ctypes.Structure.
Как писать код, похожий на Python, но компилируемый в C++ для ускорения?
Вариант - Cython. Он транслирует Python-подобный синтаксис в C++, позволяя добавлять типы для оптимизации. Пример файла cyexample.pyx:
# cyexample.pyx
def cy_add(int a, int b):
return a + b
cdef class CyPet:
cdef str name
def __init__(self, name):
self.name = name
def greet(self):
return f"Hello, {self.name}"Platform module python (модуль platform в python)
# setup.py для Cython
from setuptools import setup
from Cython.Build import cythonize
setup(
ext_modules=cythonize("cyexample.pyx"),
)Python string module (модуль string в python)
После компиляции (python setup.py build_ext --inplace) импортируйте модуль cyexample. Типизированные переменные ускоряют выполнение в десятки раз.
Проблемы:
- Необходимость установки Cython (
pip install cython). - Сложности с отладкой - ошибки Cython иногда дают нечитаемые трассировки.
- Ограниченная поддержка шаблонов C++.
Как обеспечить максимальный контроль над Python API при создании модуля?
Наиболее низкоуровневый подход - написание CPython расширения. При этом вручную определяются PyMethodDef, PyTypeObject и т.д. Пример простой функции:
// cpython_add.c
#include <Python.h>
static PyObject* add(PyObject* self, PyObject* args) {
int a, b;
if (!PyArg_ParseTuple(args, "ii", &a, &b))
return NULL;
return PyLong_FromLong(a + b);
}
static PyMethodDef Methods[] = {
{"add", add, METH_VARARGS, "Add two numbers"},
{NULL, NULL, 0, NULL}
};
static struct PyModuleDef moddef = {
PyModuleDef_HEAD_INIT, "cadd", NULL, -1, Methods
};
PyMODINIT_FUNC PyInit_cadd(void) {
return PyModule_Create(&moddef);
}Module sys python (модуль sys в python)
# setup.py для CPython расширения
from setuptools import setup, Extension
setup(
ext_modules=[Extension('cadd', sources=['cpython_add.c'])],
)
Импорт: import cadd; print(cadd.add(10, 20)).
Сложности:
- Ручное управление памятью (ссылки, сборщик мусора Python). Легко допустить утечку или segfault.
- Много boilerplate кода даже для простых функций.
- Необходимость глубокого понимания CPython API.
Расширенные примеры взаимодействия Python с C++ модулями
Пример 1: pybind11 - работа с векторами и исключениями
// vector_example.cpp
#include <pybind11/pybind11.h>
#include <pybind11/stl.h> // для автоматической конвертации std::vector
#include <vector>
#include <stdexcept>
namespace py = pybind11;
std::vector<double> scale(const std::vector<double>& v, double factor) {
std::vector<double> result;
result.reserve(v.size());
for (double x : v)
result.push_back(x * factor);
return result;
}
double safe_divide(double a, double b) {
if (b == 0)
throw std::runtime_error("Division by zero");
return a / b;
}
PYBIND11_MODULE(vecmod, m) {
m.def("scale", &scale, "Multiply each element by factor");
m.def("safe_divide", &safe_divide);
}
# test_vecmod.py
import vecmod
print(vecmod.scale([1.0, 2.0, 3.0], 2.5)) # [2.5, 5.0, 7.5]
try:
vecmod.safe_divide(10, 0)
except RuntimeError as e:
print(f"Caught: {e}") # Caught: Division by zero
[2.5, 5.0, 7.5] Caught: Division by zero
Пример 2: ctypes - передача сложных структур
// person.h
extern "C" {
typedef struct {
const char* name;
int age;
} Person;
Person create_person(const char* name, int age);
void print_person(Person* p);
}
// person.cpp
#include <cstdio>
#include <cstring>
#include "person.h"
Person create_person(const char* name, int age) {
Person p;
p.name = strdup(name);
p.age = age;
return p;
}
void print_person(Person* p) {
printf("Name: %s, Age: %d\n", p->name, p->age);
}
# person_test.py
import ctypes
lib = ctypes.CDLL('./person.so')
class Person(ctypes.Structure):
_fields_ = [("name", ctypes.c_char_p),
("age", ctypes.c_int)]
lib.create_person.restype = Person
lib.create_person.argtypes = [ctypes.c_char_p, ctypes.c_int]
p = lib.create_person(b"Alice", 30)
print(p.name.decode(), p.age) # Alice 30
lib.print_person(ctypes.byref(p)) # вывод в stdout
Alice 30 Name: Alice, Age: 30
Пример 3: Cython - использование cdef с динамической памятью
# fast_math.pyx
cdef double _square(double x) nogil:
return x * x
def square_list(list values):
cdef int i
cdef double v
cdef list result = []
for i in range(len(values)):
v = values[i]
result.append(_square(v))
return result
# setup.py
from setuptools import setup, Extension
from Cython.Build import cythonize
setup(
ext_modules=cythonize("fast_math.pyx"),
compiler_directives={'language_level': "3"}
)
# test_fast_math.py
import fast_math
print(fast_math.square_list([0.5, 1.0, 1.5, 2.0])) # [0.25, 1.0, 2.25, 4.0]
[0.25, 1.0, 2.25, 4.0]
Пример 4: CPython API - пользовательский тип с методами
// custom_type.c
#include <Python.h>
typedef struct {
PyObject_HEAD
int value;
} CustomObject;
static int Custom_init(PyObject* self, PyObject* args, PyObject* kwds) {
int val = 0;
if (!PyArg_ParseTuple(args, "|i", &val))
return -1;
((CustomObject*)self)->value = val;
return 0;
}
static PyObject* Custom_double(PyObject* self, PyObject* Py_UNUSED(ignored)) {
int v = ((CustomObject*)self)->value * 2;
return PyLong_FromLong(v);
}
static PyMethodDef Custom_methods[] = {
{"double", Custom_double, METH_NOARGS, "Return doubled value"},
{NULL, NULL, 0, NULL}
};
static PyTypeObject CustomType = {
PyVarObject_HEAD_INIT(NULL, 0)
.tp_name = "mycustom.Custom",
.tp_basicsize = sizeof(CustomObject),
.tp_itemsize = 0,
.tp_flags = Py_TPFLAGS_DEFAULT,
.tp_init = (initproc)Custom_init,
.tp_methods = Custom_methods,
};
static PyModuleDef module = {
PyModuleDef_HEAD_INIT,
.m_name = "mycustom",
.m_doc = "Example module with custom type",
.m_size = -1,
};
PyMODINIT_FUNC PyInit_mycustom(void) {
if (PyType_Ready(&CustomType) < 0)
return NULL;
PyObject* m = PyModule_Create(&module);
if (!m) return NULL;
Py_INCREF(&CustomType);
PyModule_AddObject(m, "Custom", (PyObject*)&CustomType);
return m;
}
# test_custom.py
import mycustom
obj = mycustom.Custom(42)
print(obj.double()) # 84
84
Пример 5: pybind11 - интеграция с NumPy
// numpy_example.cpp
#include <pybind11/pybind11.h>
#include <pybind11/numpy.h>
namespace py = pybind11;
py::array_t<double> add_arrays(py::array_t<double> input1, py::array_t<double> input2) {
auto buf1 = input1.request(), buf2 = input2.request();
if (buf1.ndim != 1 || buf2.ndim != 1)
throw std::runtime_error("Number of dimensions must be 1");
if (buf1.shape[0] != buf2.shape[0])
throw std::runtime_error("Shapes must match");
auto result = py::array_t<double>(buf1.shape[0]);
auto buf_r = result.request();
double *ptr1 = (double*)buf1.ptr, *ptr2 = (double*)buf2.ptr, *ptr_r = (double*)buf_r.ptr;
for (size_t i = 0; i < buf_r.shape[0]; i++)
ptr_r[i] = ptr1[i] + ptr2[i];
return result;
}
PYBIND11_MODULE(numpyadd, m) {
m.def("add_arrays", &add_arrays, "Add two numpy arrays elementwise");
}
# test_numpyadd.py
import numpy as np
import numpyadd
a = np.array([1.1, 2.2, 3.3], dtype=np.float64)
b = np.array([0.5, 1.5, 2.5], dtype=np.float64)
print(numpyadd.add_arrays(a, b)) # [1.6 3.7 5.8]
[1.6 3.7 5.8]