Сравнение 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, которая создаст новый сеанс, если в настоящий момент открытых сеансов нет.
Напоминаю: весь код этого примера можно скачать в данном репозитории.
Комментарии (8)
Shizzza
11.07.2025 11:27при помощи пакета cytpes
Прямо в самом начале очепятка. А так - спасибо за текст
Sdima1357
11.07.2025 11:27Есть ещё Python.h из python-dev и линкуется без pybind11, напрямую https://docs.python.org/3/extending/extending.html
Vilos
11.07.2025 11:27Эх чего только люди не придумают что бы не пользоваться "православным" С++....Ну коли хочешь ты C++ - пользуйся C++ на кой тебе этот "Уж" сдался? C++ самодостаточный язык и делать гибриды типа Python+C сильно сомнительный сценарий.
Vilos
11.07.2025 11:27извиняюсь не заметил, что это перевод...к автору статьи претензий нет. А что за бульон в голове у автора оригинала...не знаю.
AnonimYYYs
11.07.2025 11:27Иногда бывает такое, что есть некоторая библиотека или ранее написаный продукт на плюсах, а сейчас делается условно веб-страничка другой новой командой на питоне. И вот доходит до момента, когда новая команда понимает что им нужно воспользоваться некоторым блоком расчета, а он на плюсах, а переписывать его задача месяца на четыре да и у команды компетенций нету, а автор блока уже давно не с нами. И вот приходится выкручиваться и собирать из буханки хлеба и проволоки троллейбус
MAaxim91
По поводу перезапуска питона после перекомпиляции, думаю проблема кроется в импортирте. Возможно, если использовать importlib (importlib.reload(импортированныйпакет)), может помочь.