Сравнение 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, которая создаст новый сеанс, если в настоящий момент открытых сеансов нет.
Напоминаю: весь код этого примера можно скачать в данном репозитории.
ссылка на оригинал статьи https://habr.com/ru/articles/926644/
Добавить комментарий