一、说明
Python是一种高级编程语言,它可以调用其他语言编写的函数。在 Python 中调用 C 函数的方法有两种:1)使用 Python 提供的 ctypes 库;2)使用 Python 提供的 Cython 库。
注意:您可以在此存储库中下载此示例的完整代码。如果您对本文有任何意见,可以在那里开始一个问题,或与我联系。
二、PyBind11 vs ctypes
基本上有两种方法可以从 Python 调用C++:使用 PyBind11 C++ 库生成 Python 模块,或者使用 cytpes Python 包访问编译的共享库。使用 PyBind11,我们可以更轻松地共享许多数据类型,而使用ctypes 是一种低级的 C 样式解决方案。
就我而言,我希望能够利用C++性能和可移植性,但我不想放弃解释语言的交互性以进行快速探索和调试。
幸运的是,从Python调用C++并不像一开始看起来那么困难。这样,我们可以在开发C++代码的同时掌握 Python 的一些交互性。
就我而言,我想使用 Python 来:
- 将一些问题参数传递给C++
- 调用C++代码以运行计算密集型例程
- 检索最终结果,以及一些用于调试的中间计算。
- 以交互方式浏览结果,并生成绘图和报告。
使用 ctypes 的问题在于,共享许多数据类型需要相当多的低级解决方法。例如,虽然ctypes不支持复数等基本的东西,但PyBind11使Numpy与Eigen完全互操作,需要最少的代码。
但是,我也发现了PyBind11 的小问题。事实证明,在重新编译C++代码并尝试重新加载 PyBind 生成的 Python 模块后,什么也没发生。重新加载编译模块的唯一方法是重新启动我的 Python 会话。无论如何,这没什么大不了的,因为Python的启动时间可以忽略不计。而且,此步骤可能在 IDE 级别自动执行。
因此,现在的问题是如何充分利用 PyBind11。
三、与 PyBind11 共享C++类
PyBind11 的官方文档非常出色,我可以毫无问题地开始使用它。但是,我想分享这个库的超级快速入门指南,以及我打算如何使用它。
Pybind11 是一个仅标头库,你可以通过以下方式获取它:
pip install pybind11
虽然没有必要将所有C++代码构建为一个类,但如果你有一个类要在C++和 Python 之间共享,Pybind11 会让你的事情变得非常容易。(其实我更像是那种人,总是想介绍给定项目中最少的类数)vector
struct
然而,在这种情况下,我发现使用外观设计模式(参见wiki)可以同时导致非常简单的Python/C++互操作性和一个不错的API。
所以,我想出了一个简单的课程。它基本上包含:
- 读取问题参数的构造函数。
- 执行计算的函数。
run()
- 一些数组作为公共变量来存储结果。
Eigen
这是我的最小示例:
// mylib.h#include #include using Eigen::Matrix, Eigen::Dynamic;typedef Matrix<std::complex, 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 #include #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_(m, "MyClass").def(py::init()).def("run", &MyClass::run, py::call_guard()).def_readonly("v_data", &MyClass::v_data, byref).def_readonly("v_gamma", &MyClass::v_gamma, byref);}
需要强调的几点:
- 类构造函数签名指定为
.def(py::init())
- 对于函数,我们希望释放 GIL(全局解释器锁),这将阻止我们的函数使用多个线程。
run()
最后,可以使用以下文件进行编译: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++库。
四、从 Python 调用C++库
这个过程非常简单,应该开箱即用。但是,有几个步骤可以优化交互式工作流,这些步骤稍微棘手一些,也值得实施。
例如,如果您正在执行 Python 环境并且您的编译库进入一个目录,您可以执行以下操作:build
import syssys.path.append("build/")from MyLib import MyClassimport matplotlib.pyplot as pltSimulation = 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()
请注意,特征向量会自动转换为Python 数组。
Ater 修改 ,每个我们要公开的新函数或变量只需要添加一行代码。myLib.hpp
pywrap.cpp
不幸的是,这不会带来完全交互式的工作流程。当您在更改后重新编译C++代码时,Python 端不会发生任何事情。即使您尝试使用以下方法重新加载 Python 模块:importtools
import importlibimportlib.reload(MyLib)
什么也没发生。原因是编译后的代码无法在 Python 中重新加载。
因此,在使用 PyBind11 时,每次重新编译C++代码时都需要重新启动 Python 会话,我觉得这对于开发目的来说有点烦人。但是,这是一个很小的代价,因为Python的启动时间可以忽略不计,并且可能有一种方法可以使用一些IDE热键或其他工具使该过程自动化。
五、总结
因此,这就是您可以轻松地从 Python 调用C++库的方式。
特别是,这个两步过程可以产生一个非常互动的开发工作流程。尽管我们有一个编辑-编译-运行工作流,但我们在最后添加了一个解释器,所以现在我们的工作流看起来像编辑-编译-运行-探索。
将来,我计划将两个功能合并到此工作流中:
- 第一个是C++20模块,它应该加快大型C++项目的编译时间。不幸的是,CMake 仍然与模块不兼容(有关更新,请参阅此问题),显然人们必须依靠像Ninja-Build这样的构建系统才能立即提供此功能。
- 另一件事是解决在重新编译C++代码后(手动)重新启动 Python 会话的需要。为此,我希望也许可以在VSCode级别对此做点什么。到目前为止,VS Code 中的最佳选项似乎是终止 Python 会话,然后使用 执行 Python 代码,如果尚未打开会话,则会创建一个新会话。
Shift+Enter