Сравнение PyBind11 vs ctypes

В принципе, можно вызывать C++ из Python двумя способами: при помощи библиотеки PyBind11 для C++, которая готовит модуль Python, либо при помощи пакета cytpes для Python, который предоставляет доступ к скомпилированной разделяемой библиотеке. Работая с PyBind11, не составляет труда совместно использовать множество типов данных, в то время как ctypes — это гораздо более низкоуровневое решение в стиле C.

Взявшись за описанный здесь проект, я хотел рассчитывать на производительность и переносимость C++, но так, чтобы не жертвовать интерактивностью интерпретируемых языков, которая удобна для экспресс-исследования и отладки.

К счастью, вызывать C++ из Python не так сложно, как может показаться на первый взгляд. Таким образом, можно в какой-то степени позаимствовать интерактивность Python при разработке кода C++.

Вот для чего я хотел использовать Python в данном конкретном случае:

  • Передавать в C++ некоторые проблемные параметры 

  • Вызывать код C++ для выполнения ресурсозатратных процедур

  • Извлекать окончательные результаты, а также, в отладочных целях — некоторые промежуточные вычисления.

  • Исследовать результаты в интерактивном режиме, строить на их основе графики и отчёты.

При использовании ctypes возникает такая проблема: для совместного использования множественных типов данных требуется немало низкоуровневых обходных манёвров. Например,  ctypes не поддерживает таких элементарных вещей, как комплексные числа, а PyBind11 обеспечивает полное взаимодействие Numpy с Eigen, и на это требуется минимум кода.

Правда, я обнаружил и небольшую проблему с PyBind11. Оказывается, что после перекомпиляции кода C++ и при попытке перезагрузить сгенерированный PyBind модуль Python ничего не происходит. Был только один действующий способ перезагрузить скомпилированный модуль — перезапустить сеанс Python. В любом случае, всё это несложно, Python запускается почти сразу. Вероятно, этот шаг можно автоматизировать на уровне IDE.

Итак, нас интересует, как выжать максимум из PyBind11.

Совместное использование класса C++ при работе с PyBind11

Официальная документация у PyBind11 просто отличная, и я без проблем приступил к работе, опираясь на неё. Но заранее хочу поделиться отличным руководством по быстрому запуску этой библиотеки, а ещё расскажу, как собираюсь этой библиотекой пользоваться.

Библиотека Pybind11 содержит только заголовочный файл, и получить её не составляет труда:

pip install pybind11

Нет необходимости структурировать весь ваш код на C++ как класс. Pybind11 сильно упростит вам жизнь, если у вас есть класс, который можно совместно использовать сразу в C++ и Python. (Кстати, я предпочитаю использовать vector, а не struct, причём, в порученных мне проектах стараюсь обойтись минимальным количеством классов).

Но в данном случае я пришёл к выводу, что, применив паттерн проектирования Фасад, можно одновременно и обеспечить очень простое взаимодействие между Python и C++, и сделать приятный API.

Таким образом, у меня получился простой класс. В сущности, он содержит:

  • Конструктор, читающий параметры задачи

  • Функцию run(), выполняющую вычисление

  • Несколько массивов Eigen, используемых в качестве публичных переменных для хранения результатов

Вот мой минимальный пример:

// mylib.h
#include <Eigen/Dense>
#include <cmath>

using Eigen::Matrix, Eigen::Dynamic;
typedef Matrix<std::complex<double>, Eigen::Dynamic, Eigen::Dynamic> myMatrix;

class MyClass {

    int N;
    double a;
    double b;

public:

    Eigen::VectorXd v_data;
    Eigen::VectorXd v_gamma;

    MyClass(){}
    MyClass( double a_in, double b_in, int N_in) 
    {
        N = N_in;
        a = a_in;
        b = b_in;
    }

    void run() 
    { 
        v_data = Eigen::VectorXd::LinSpaced(N, a, b); 

        auto gammafunc = [](double it) { return std::tgamma(it); };
        v_gamma = v_data.unaryExpr(gammafunc);
    }
};

Для совместного использования этого класса потребуется добавить немного кода на C++. Предпочитаю сделать это в отдельном файле, в котором будет всё, что необходимо для создания обёртки на Python.

// pywrap.cpp
#include <pybind11/pybind11.h>
#include <pybind11/eigen.h>
#include "mylib.h"

namespace py = pybind11;
constexpr auto byref = py::return_value_policy::reference_internal;

PYBIND11_MODULE(MyLib, m) {
    m.doc() = "optional module docstring";

    py::class_<MyClass>(m, "MyClass")
    .def(py::init<double, double, int>())  
    .def("run", &MyClass::run, py::call_guard<py::gil_scoped_release>())
    .def_readonly("v_data", &MyClass::v_data, byref)
    .def_readonly("v_gamma", &MyClass::v_gamma, byref)
    ;
}

Что здесь хотелось бы отметить:

  • Сигнатура конструктора класса указывается при помощи .def(py::init<int, double, double>())

  • Для функции run() потребуется снять глобальную блокировку интерпретатора (GIL), которая не позволяет нашей функции использовать по несколько потоков.

Наконец, этот код можно скомпилировать на основе следующего файла CMakeLists.txt:

cmake_minimum_required(VERSION 3.10)

project(MyLib)
set(CMAKE_CXX_STANDARD 20)
set(PYBIND11_PYTHON_VERSION 3.6)
set(CMAKE_CXX_FLAGS "-Wall -Wextra -fPIC")

find_package(pybind11 REQUIRED)
find_package(Eigen3 REQUIRED)

pybind11_add_module(${PROJECT_NAME} pywrap.cpp)

target_compile_definitions(${PROJECT_NAME} PRIVATE VERSION_INFO=${EXAMPLE_VERSION_INFO})
target_include_directories(${PROJECT_NAME} PRIVATE ${PYBIND11_INCLUDE_DIRS})
target_link_libraries(${PROJECT_NAME} PRIVATE Eigen3::Eigen)

Всё готово к работе. Если вы работаете с VS Code, то, сконфигурировав расширение CMake, просто нажмите F7 — и ваша библиотека C++ скомпилируется.

Вызов библиотеки C++ из Python

Здесь всё совсем просто и должно работать прямо из коробки. Но поток задач интерактивный, и в нём есть несколько шагов, поддающихся оптимизации. Эти оптимизации реализовать несколько сложнее, но дело того стоит.

Например, если вы выполняете вашу среду Python, и скомпилированная вами библиотека поступает в каталог build, можно сделать так:

import sys
sys.path.append("build/")
from MyLib import MyClass

import matplotlib.pyplot as plt

Simulation = MyClass(-4,4,1000)
Simulation.run()

plt.plot(Simulation.v_data, Simulation.v_gamma, \
"--", linewidth = 3, color=(1,0,0,0.6),label="Function Value")
plt.ylim(-10,10)
plt.xlabel("x")
plt.ylabel("($f(x) = \gamma(x)$)")
plt.title("(Gamma Function: $\gamma(z) = \int_0^\infty x^{z-1} e^{-x} dx$)",fontsize = 18);
plt.show()

Обратите внимание, что Eigen-векторы были автоматически преобразованы в массивы Python.

Модифицировав myLib.hpp, остаётся добавить в файл pywrap.cpp всего по одной строке кода на каждую новую функцию или переменную, которые вы хотите предоставлять.

К сожалению, полностью интерактивный поток задач таким образом построить не удастся. Когда вы перекомпилируете ваш код C++ после изменений, на стороне Python ничего не произойдёт. Даже если вы попытаетесь перезагрузить модуль Python при помощи importtools:

import importlib
importlib.reload(MyLib

— ничего не произойдёт. Дело в том, что скомпилированный код перезагрузке в Python не поддаётся.

Таким образом, при работе с PyBind11 вам придётся перезапускать сеанс Python после каждой перекомпиляции кода C++ — на этапе разработки меня это довольно раздражает. Тем не менее, это вполне приемлемо, так как Python запускается почти сразу, и весь процесс, пожалуй, можно автоматизировать при помощи горячих клавиш IDE или других инструментов.

Резюме

Вот вы и узнали, как без труда вызывать библиотеку C++ из Python.

В частности, такой двухэтапный процесс помогает наладить процесс разработки, отличающийся высокой интерактивностью. Пусть он и построен по принципу «отредактировать-скомпилировать-запустить», в конце этой цепочки мы добавили интерпретатор, поэтому теперь наш поток задач приобретает вид «отредактировать-скомпилировать-запустить-исследовать».

Полагаю, в будущем надо как-то избавиться от необходимости (вручную) перезапускать сеанс Python после перекомпиляции кода C++. Надеюсь, эта проблема как-то решается на уровне VSCode. До сих пор лучшее, что можно сделать для этого в VSCode — принудительно завершить сеанс Python, а затем выполнить код Python командой Shift+Enter, которая создаст новый сеанс, если в настоящий момент открытых сеансов нет.

Напоминаю: весь код этого примера можно скачать в данном репозитории.

Комментарии (9)


  1. MAaxim91
    11.07.2025 11:27

    По поводу перезапуска питона после перекомпиляции, думаю проблема кроется в импортирте. Возможно, если использовать importlib (importlib.reload(импортированныйпакет)), может помочь.


  1. Shizzza
    11.07.2025 11:27

    при помощи пакета cytpes

    Прямо в самом начале очепятка. А так - спасибо за текст


  1. edo1h
    11.07.2025 11:27

    На всякий случай напомню, что рекомендациям не использовать C/C++ в новых проектах уже несколько лет


    1. Xiran
      11.07.2025 11:27

      Ну избавляйтесь от всех зависимостей тоже, переписываете то что работает на здоровье


    1. Valor760
      11.07.2025 11:27

      Можно поинтересоваться, кто эти рекомендации выдвинул? Или мы говорим про одну статью от Нац Агенства Безопасности США?

      Ну хорошо, давайте ядро ОС на джаве писать. Ах да, ведь есть же всеми любимый раст... И он точно не имеет проблем!


  1. Sdima1357
    11.07.2025 11:27

    Есть ещё Python.h из python-dev и линкуется без pybind11, напрямую https://docs.python.org/3/extending/extending.html


  1. Vilos
    11.07.2025 11:27

    Эх чего только люди не придумают что бы не пользоваться "православным" С++....Ну коли хочешь ты C++ - пользуйся C++ на кой тебе этот "Уж" сдался? C++ самодостаточный язык и делать гибриды типа Python+C сильно сомнительный сценарий.


    1. Vilos
      11.07.2025 11:27

      извиняюсь не заметил, что это перевод...к автору статьи претензий нет. А что за бульон в голове у автора оригинала...не знаю.


    1. AnonimYYYs
      11.07.2025 11:27

      Иногда бывает такое, что есть некоторая библиотека или ранее написаный продукт на плюсах, а сейчас делается условно веб-страничка другой новой командой на питоне. И вот доходит до момента, когда новая команда понимает что им нужно воспользоваться некоторым блоком расчета, а он на плюсах, а переписывать его задача месяца на четыре да и у команды компетенций нету, а автор блока уже давно не с нами. И вот приходится выкручиваться и собирать из буханки хлеба и проволоки троллейбус