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将会依次匹配,找到最合适的重载函数。如下,展示了重载函数的绑定,其参数类型分别为
int
和 double
。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_readwrite
或class_::def_property
定义的。要让C++类也支持动态属性,需要在
py::class_
的构造函数添加py::dynamic_attr
标识,如下:py::class_<Pet>(m, "Person", py::dynamic_attr()) .def(py::init<>()) .def_readwrite("name", &Pet::name);