前言
Python作为一门小学生都需要掌握的语言1[, 2],其重要性对于我等社畜不言而喻。然而如果遇到抠门的老板舍不得买更好的机器,我们就只能看着不着边际的运行时间和不足的内存喟然长叹。但看到分割字符串都有100种写法的C++[3],又觉得右下腹某个器官在隐隐作痛
神说,要5点半下班
于是,所以有了pybind11[4]。
当然前面说能让你5点半下班只有梦里才会有,但让机器快点跑完程序还是有一些办法的。Python的性能问题是广大码农和科研民工争论的热门话题5, 6[, 7],不过有了NumPy、PyPy[8]的现在,这并不是一个很大的问题,毕竟快速出活早点下班咸鱼才是最重要的事情。
然而这些方法并不是万灵药。例如我们自己设计的算法本身难以用库提供的API实现,或者已经有了C++实现的久经考验的遗留代码,同时又难以割舍Python的易用性,这就是pybind11该出场的时候了。它提供了一个融合Python和C++代码的简便方法,特别是对于已有的代码,并不需要修改,只需要相应编写完成相关转换的接口即可。这一点还另外有一个非常实用的优势:除pybind11接口相关文件以外C++代码部分,能完全在C++的工作流中开发,可以充分利用各种IDE和调试工具(针对的就是你,Cython[9])因此,如果你符合下列的一条或更多,那就建议你继续阅读下去:
- 已有成熟的C++代码基础,并希望能够在Python中使用
- 希望提升现有Python代码的性能,而不是用另外的语言推倒重来
- 你的新项目希望在追求极致性能的同时保持易用性,毕竟Python是小学生都会的语言
本文主要介绍pybind11的用法,不会涉及其底层原理(并不会承认我也不懂),想要深入了解请参阅4[, 10]。
多说一句,TensorFlow和PyTorch的通信接口现在也是基于pybind11的[11],想和大佬们一样走上人生巅峰吗,那就快来学习吧
Hello World
Step 1 环境搭建
困扰计算机初学者的一个重要问题就是环境怎么搭,对于我这种菜鸟也不例外。因此首先准备原材料:
一根能上网的网线- 支持至少C11的C++编译器,Python解释器
- CMake(重要!)
- IDE(可选,毕竟真正的大神都拿文本文档写代码)
如果能顺利基于CMake用C++打印出Hello World,也能跑起Python,那么就可以进入下一步骤。
Step 2 项目结构及依赖
下载依赖
下载pybind11头文件
git clone https://github.com/pybind/pybind11.git
下载pybind11/cmake样板代码
git clone https://github.com/pybind/cmake_example.git
创建项目
这里我们采用把pybind11的头文件直接包含在项目目录中的方法,而不是在系统范围内安装依赖。至于说原因?大概是洁癖吧。(不过这里还是有切实好处的,比如项目文件复制到另外一个环境中,不需要太多配置就能够跑起来)
创建项目文件夹
mkdir awesome_project
将 cmake_example 中的文件一股脑扔进我们的项目文件
cp -r cmake_example/* awesome_project/
把pybind11也放进来
mkdir awesome_project/3rd/pybind11
将前述pybind11文件夹下的 include, tools 和 CMakeList.txt 复制到我们的项目路径中
cp -r pybind11/include awesome_project/3rd/pybind11/includecp -r pybind11/tools awesome_project/3rd/pybind11/toolscp pybind11/CMakeLists.txt awesome_project/3rd/pybind11/CMakeLists.txt
此时项目结构为
awesome_project|-- 3rd| +- pybind11| |-- include| | +- ...| |-- tools| | +- ...| +- CMakeLists.txt|-- src| +- main.cpp|-- setup.py|-- CMakeLists.txt+- ...
Step 3 Hello World
添加完文件后,我们还要对最外面的 CMakeLists.txt 做一个小小的调整
cmake_minimum_required(VERSION 2.8.12)project(cmake_example)add_subdirectory(3rd/pybind11)pybind11_add_module(cmake_example src/main.cpp)target_compile_features(cmake_example PUBLIC cxx_std_14)
最后
pip install .
终于来到了激动人心的时刻,打开Python解释器
>>> import cmake_example>>> cmake_example.add(1, 2)3
这样就走完了整个流程。
使用方法
众所周知,主流的Python解释器CPython使用的是C语言,同时也有加载外部模块的能力。要想加载C++写成的外部模块,所需要做的仅仅只是按照设计写一点点接口代码
一个简单示例(并不)
#define PY_SSIZE_T_CLEAN#include <Python.h>static PyObject *spam_system(PyObject *self, PyObject *args){const char *command;int sts;if (!PyArg_ParseTuple(args, "s", &command))return NULL;sts = system(command);return PyLong_FromLong(sts);}
为了让“仅仅”名副其实,我们看我们的主角是怎么做的。进入 src/main.cpp
#include <pybind11/pybind11.h> // 引入头文件int add(int i, int j) { // 我们的绝妙算法return i + j;}namespace py = pybind11;PYBIND11_MODULE(cmake_example, m) {m.doc() = R"pbdoc(Pybind11 example plugin-----------------------.. currentmodule:: cmake_example.. autosummary:::toctree: _generateaddsubtract)pbdoc"; // 宣称我们的算法能够改变世界m.def("add", &add, R"pbdoc(Add two numbersSome other explanation about the add function.)pbdoc"); // 告诉Python解释器如何使用我们的算法m.def("subtract", [](int i, int j) { return i - j; }, R"pbdoc(Subtract two numbersSome other explanation about the subtract function.)pbdoc"); // lambda也可以#ifdef VERSION_INFOm.attr("__version__") = VERSION_INFO; // 属性同样是可以的#elsem.attr("__version__") = "dev";#endif}
上述代码我们抽取最核心的几句
PYBIND11_MODULE(cmake_example, m) {m.def("add", &add);}
第1行是声明接口的宏定义,告诉编译器里面的内容描述了模块的接口,第2行定义了一个函数的接口,参数"add"是Python中看到的函数名,第二个参数&add是C++中函数的地址。
更多用法
当然,仅为函数创建接口并不能满足饥渴的性能狂魔,对于类,也有着非常简便的定义方法。
假设我们有这样一个类
class Calc {private:double version;public:Calc(double version) : version(version) {}double subtract(double a, double b) {return a - b;}};
对应的接口声明为(别忘了放在PYBIND11_MODULE中)
py::class_<Calc>(m, "Calc") // 类名,注意模板参数.def(py::init<double>(), py::arg("version")) // 构造函数,py::init的模板参数用于指定构造函数的参数类型.def("subtract", &Calc::subtract, py::arg("a"), py::arg("b")); // 注意只有这里有分号,整个接口描述采用的是链式定义
到这里,我们已经覆盖了相当一部分python调用C++的功能需求。对比原始的Python接口代码,使用pybind11显然能帮我们保住更多的头发,特别是不用了解CPython中各类对象的细节。实际上,如果适当地应用现代C++的特性,效率上也会有不少改进,同时C++的整个生态系统也触手可及了。
如这些还满足不了你,请参阅pybind11的文档[4],和一些不错的资料12[, 13]。
实践建议
在前面,我们也曾提及到pybind11有一个重要的优势能够在独立C++工作流中开发,这其实也是我做出选择的最重要原因。其他的方法在这方面要么是压根无法脱离Python使用,要么就是有着其他缺陷。而选用本文这种项目组织方式,某种程度上也正是为这一目的服务。下面就介绍一个使用案例。
同样基于我们之前的项目代码,为了更具有代表性,我们在项目目录中新建 include/algo.h,把 main.cpp 中的算法部分移动到 include/algo.h 中,并修改为引入头文件
// include/algo.hint add(int i, int j) { // 我们的绝妙算法return i + j;}
main.cpp 修改为
#include <pybind11/pybind11.h> // 引入头文件#include "algo.h"namespace py = pybind11;// 后面一切如前
在 src 中新建 debug.cpp
#include <iostream>#include "algo.h"int main() { // 一切如常std::cout << add(1, 2) << std::endl;return 0;}
将前面的 CMakeLists.txt 修改为
cmake_minimum_required(VERSION 2.8.12)project(cmake_example)add_executable(debug src/debug.cpp)target_include_directories(debug PUBLIC include)add_subdirectory(3rd/pybind11)include_directories(include PUBLIC)pybind11_add_module(cmake_example src/main.cpp)target_compile_features(cmake_example PUBLIC cxx_std_14)
这样在IDE中以此 CMakeLists.txt 初始化项目时,就有了debug这一目标,我们就可以随意调戏我们的算法了。
注意,这个流程是不需要任何与Python相关的依赖出现的,也就是说我们可以在 algo.h 中写好算法,在debug.cpp 写各种测试代码,并充分利用包括断点调试,性能分析等各类工具。调试完成后,再完成 main.cpp 中的接口代码。这使得我们能够很大程度上避免跨语言调试的复杂性。
安利时间
阅读到这里的读者有没有感觉这个流程非常头大,不要怕,快来使用最新出炉的样板代码生成工具,不要998,不要98,只要一行命令抱回家,使用
pip install sebae
即刻获得,生成示例代码同样极为简单
sebae init
赶快来试用吧!
参考资料
[1] 如何看待小学生开始学Python
[2] 当当网正版童书 DK编程真好玩全套2册 中国小学生分级阅读书目 零基础编程启蒙书全新Scratch 3.0升级Python版 (请结一下广告费谢谢)
[3] C++ 的 std::string 有什么缺点?
[4] pybind11 — Seamless operability between C++11 and Python
[5] 为什么Python比C++慢很多?
[6] 同为动态语言,Python 的性能为何只有 PHP 的五分之一?
[7] 如何看待 python 的性能?
[8] A fast, compliant alternative implementation of Python
[9] Debugging your Cython program
[10] PyBind11:基本用法和底层实现
[11] Replace SWIG with pybind11
[12] 使用pybind11 将C++代码编译为python模块
[13] pybind11的最佳实践
