Расширение возможностей Python через модули на C++

Раздел: Основы Python -> Работа с модулями

Основные подходы к созданию 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())              # Rex

Python 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))  # 42

Python 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 typing module (модуль typing в python)
- Python module windows (модуль windows для python)
- Python module path (путь к модулю python)

Расширенные примеры взаимодействия 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]

Взаимодействие Python с модулями C++ - comments

En
Python cpp module (python)