Pybind11示例

Cover
C++与Python混合编程的利器,可以很方便的把C++中的接口暴露给Python调用,以满足一些场景下的性能要求,即把C++ 代码编译成 Python 模块。官方介绍如下:
pybind11 is a lightweight header-only library that exposes C++ types in Python and vice versa, mainly to create Python bindings of existing C++ code. Its goals and syntax are similar to the excellent Boost.Python library by David Abrahams: to minimize boilerplate code in traditional extension modules by inferring type information using compile-time introspection.

环境配置

本示例所运行平台为wsl2 Ubuntu-20.04。以下是一些必要的包:
  • pybind11。由于CPP混乱的包管理体系,所以推荐conda或者pip来安装(当然也可以通过源码安装)。
  • python3-dev。sudo agt-get install python3-dev
  • cmake。sudo agt-get install cmake
示例将有两个文件,分别为:
  • example.py 用来测试pybind绑定cpp代码后生成的动态链接库。
  • mylib.cpp CPP代码,将通过pybind11生成.so动态链接库
cpp将通过cmake构建,工程文件结构如下:
-proj -src mylib.cpp example.py CMakeLists.txt
CmakeList.txt如下,使用时需要根据具体设备配置 pybind11_DIR 路径和
cmake_minimum_required(VERSION 3.2) project(Pybind11_Example C CXX) # find python execute_process(COMMAND python3-config --prefix OUTPUT_VARIABLE Python_ROOT_DIR) find_package(Python COMPONENTS Development Interpreter REQUIRED) include_directories(${Python_INCLUDE_DIRS}) # find pybind execute_process(COMMAND python3 -m pybind11 --cmakedir RESULT_VARIABLE __pybind_exit_code OUTPUT_VARIABLE pybind11_DIR OUTPUT_STRIP_TRAILING_WHITESPACE) set(pybind11_DIR /home/brc/miniconda3/envs/dlsys/lib/python3.8/site-packages/pybind11/share/cmake/pybind11) find_package(pybind11) include_directories(SYSTEM ${pybind11_INCLUDE_DIRS}) list(APPEND LINKER_LIBS ${pybind11_LIBRARIES}) add_library(mylib MODULE src/mylib.cpp) target_link_libraries(mylib PUBLIC ${LINKER_LIBS}) pybind11_extension(mylib) pybind11_strip(mylib) set_target_properties(mylib PROPERTIES LIBRARY_OUTPUT_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} )

函数的绑定

#include <pybind11/pybind11.h> namespace py = pybind11; int sum(int a, int b){ return a + b; } PYBIND11_MODULE(mylib, m){ m.doc() = "My library"; m.def("sum", sum, "A function which adds two numbers", py::arg("a")=1, py::arg("b")=2); }
前两行为头文件以及命名空间约定俗成的写法,也可以根据需要自行添加头文件,如pybind11/numpy.h等头文件
PYBIND11_MODULE会创建一个函数,它在Python中使用import语句时被调用。其第一个参数是模块名(mylib,即Python中import时使用到的模块名,不要用引号包住!);第二个参数是类型为py::module_的变量(m),这是创建绑定的主要接口。使用module_::def()方法,则会生成对应函数的Python绑定代码。
m.doc() = "My library"; 定义了该模块的文档。在python中可以通过 mylib.__doc__ 或者 help(mylib)查看。
m.def("sum", &sum, "A function which adds two numbers", py::arg("a")=1, py::arg("b")=2); 上述代码绑定了 一个函数,该函数暴露给Python的函数名称为 "sum",实际执行的函数为sum,函数文档为 "A function which adds two numbers"py::arg("a")=1, py::arg("b")=2 为关键字参数及默认参数值。 在绑定函数m.def中,只有前两个参数是必须的,之后的文档、关键字参数、默认参数均可以省略。
💡
Note:函数入参和返回值相关的细节都由模板元编程自动推断。
上述代码在编译完成后,会生成一个动态链接库,如本例中将生成 mylib.cpython-38-x86_64-linux-gnu.so,在Python中可通过import导入,并可以直接使用暴露出来的add函数。
import mylib print(mylib.__doc__) a = 12 b = 23 c = mylib.sum(a, b) print(c) # 输出如下: >> My library >> 35

重载函数的绑定

在绑定重载函数时,我们需要增加函数签名相关的信息以消除歧义。绑定多个函数到同一个Python名称,将会自动创建函数重载链。Python将会依次匹配,找到最合适的重载函数。如下,展示了重载函数的绑定,其参数类型分别为 intdouble
m.def("sum", py::overload_cast<int, int>(&sum), "A function which adds two int numbers"); m.def("sum", py::overload_cast<double, double>(&sum), "A function which adds two double numbers");
这里,py::overload_cast仅需指定函数类型,不用给出返回值类型。
使用时,可以直接调用 sum函数来执行计算。

使用stl容器作为参数的函数的绑定

pybind11提供了stl容器的封装类,当需要处理stl容器是,只要额外包括头文件<pybind11/stl.h>即可。在使用时候,pybind11会自动进行类型转换,具体的,转换包括:
  • std::vector<>, std::list<>, std::array<> 转换成 Python list
  • std::set<>, std::unordered_set<> 转换为 Python set
  • std::map<>, std::unordered_map<> 转换成Python dict
因为函数入参和返回值相关的细节都由模板元编程自动推断,且会自动转换,因此使用上没有太多差别。

class和struct的绑定

简单 class 绑定

class Person{ public: Person(const std::string & name): name(name){ }; void setName(const std::string & name){ this->name = name; } const std::string & getName() const { return this->name; } std::string name; }; PYBIND11_MODULE(mylib, m){ m.doc() = "My library"; m.def("sum", sum, "A function which adds two numbers"); py::class_<Person>(m, "Person") .def(py::init<const std::string &>()) .def("setName", &Person::setName) .def("getName", &Person::getName); }
class_<>会创建C++ class或 struct的绑定,,其中<>中的参数为C++代码中的命名空间::类名,紧接着()中参数的含义为(m, "在python中构造这个类的方法名" )
init()方法使用类构造函数的参数类型作为模板参数,并包装相应的构造函数。
关键字参数和默认参数同
👉
类的构造函数需要手动绑定,而析构函数会自动绑定,且会自动被 Python 的内存回收机制调用。

动态属性

原生的Pyhton类可以动态地获取新属性。但是,默认情况下,从C++导出的类不支持动态属性,其可写属性必须是通过class_::def_readwriteclass_::def_property定义的。
要让C++类也支持动态属性,需要在py::class_的构造函数添加py::dynamic_attr标识,如下:
py::class_<Pet>(m, "Person", py::dynamic_attr()) .def(py::init<>()) .def_readwrite("name", &Pet::name);

参考文献