前言
pybind11作为轻量级、非侵入式的库,用于在C++和Python之间创建绑定。
基于C++11特性设计,语法简洁,支持自动类型转换和STL容器,无需额外依赖。
适合高性能科学计算、跨平台通用库模块等绑定
但无QT支持,所以Qt相关类得手动绑定
在此记录一下将C++项目中的一些接口绑定到python的经验
一、主要用法
pybind11 官方介绍及基本用法 doc
1. 绑定基础框架
大概分为这么几步:
引入pybind11库
- 前置准备:准备 pybind11 文件
- python 导入:
pip install ...
- git 子模块导入:
git submodule add ...
(从用户负担、管理与后续维护等方面考虑,这个方式更推荐) - 如果上述方式都不行(比如测试环境没法联网也安装不了包),还有个办法就是单独准备 pybind11 的头文件(实在不行单独把代码复制过去也行,毕竟也就
30+
个头文件嘛对吧)
- python 导入:
- 导入包:在项目
CMakeLists.txt
中配置包路径- python:自动搜索包路径
find_package(pybind11 REQUIRED)
- git 子模块:
add_subdirectory(${path_to_pybind11})
- 仅头文件:这个配置一下头文件路径就行,放在项目中假装是子模块
set(PYBIND_INCLUDE_DIR ...)
- 前置准备:准备 pybind11 文件
编写绑定文件
作为非侵入式的库,pybind11可以在不改动源代码的情况下绑定接口,所以也可以用于已有动态库的拓展
就一般项目而言,一个类对应一个绑定文件打包&链接
不同引入方式也就打包代码稍微不同,链接代码是一样的
反正先设置一下模块名,然后打包:set(module_name your_module_name)
- python/git:
pybind11_add_module(${module_name} xxx.cpp)
- 仅头文件:添加类的实现文件
add_library(${module_name} SHARED xxx.cpp // include implementation file xxx_binding.cpp // binding file )
注:注意
SHARED
与MODULE
的区别,如果模块间涉及依赖关系,或者想程序启动时就加载这些python模块,需要使用SHARED
- 链接:如果是只有头文件,则不用链接
pybind11::module
;如果这个模块依赖其他python模块,也需要把所需模块名加在这里
# include python & pybind11 headers path target_include_directories(${module_name} PRIVATE ... ) # link pybind11 and other necessary libraries target_link_libraries(${module_name} PRIVATE pybind11::module ... ) # change suffix if needed, and set output path set_target_properties(${module_name} PROPERTIES PREFIX "" SUFFIX ".pyd" LIBRARY_OUTPUT_DIRECTORY "path_to_output" ) # Windows or (SUFFIX ".so") for Linux/macOS
注:这里
set_target_properties
设置的模块名仅为磁盘上的文件名,实际模块名以绑定文件中PYBIND11_MODULE
定义的为准编译生成动态库
.pyd
,在python中导入即可
2. 绑定语法
2.1 基础定义
绑定文件基本包括以下几个部分:
// pybind11 header and other headers
#include <pybind11/pybind11.h>
#include "..."
// namespace for convenience
namespace py = pybind11;
// define your module name
PYBIND11_MODULE(your_module_name, m) {
// helping doc for your module
m.doc() = "binding doc";
// define functions/variables/classes in your module (module level)
m.def("add", &add, "A function which adds two numbers");
}
注1:因为一般是要绑定类,所以需要include
对应类的头文件
注2:PYBIND11_MODULE
用于定义一个模块,一个模块 只可 用其定义一次,否则会出现重复定义的问题
注3:这里的模块名your_module_name
就是python代码中import
时使用的模块名
注4:直接在PYBIND11_MODULE
层使用m.def()
定义的对象的级别属于 模块级别 ,像是全局变量、全局函数都这样定义,类的静态方法之类也可以这样定义(但要注意变量名污染的问题)
注5:绑定类相关的代码一般就放在一行(仅一个;
),所以下面绑定代码一般以.def...
开始而没有;
结尾,只要记住最后加上;
就行
2.2 C++类及类相关定义
假设有一个类A
定义了一个二维点,它包含以下信息:
- 点的坐标
x
、y
,以及私有信息data_
- 一些构造函数
- 重载了一些与坐标相关的操作符
- 一些方法
A
类的头文件示例:(方法什么的我就不写了,反正就是操作这几个成员)
class A{
private:
int data_;
public:
static const A ORIGIN; // 坐标原点
int x;
int y;
...
};
2.2.1 类、构造函数
// define module
PYBIND11_MODULE(your_module_name, m) {
// declare your class
py::class_<A>(m, "A")
// define constructors
.def(py::init<>()) // default constructor
.def(py::init<int, int>()) // constructor with 2 int parameters;
...
}
2.2.2 类成员
可读可写用readwrite
,只读用readonly
,静态则加_static
// public member
.def_readwrite("x", &A::x, "X coordinate of the point")
// public static const member
.def_readonly_static("ORIGIN", &A::ORIGIN)
// protected/private member with getter/setter
.def_property("data", &A::getData, &A::setData)
2.2.3 类方法
指定方法名和对应C++类中调用的方法名即可
另外可通过py::arg
设置参数名、添加默认参数等
// normal function
.def("xxx", &A::xxx)
.def("xxx", &A::xxx, py::arg("arg_name") = DEFAULT_ARG_VALUE)
// static function
.def_static("xxx", &A::xxx)
还可以利用lambda 表达式
自己实现相关魔法方法:
// define how to print a object
.def("__repr__", [](const A &p) {
return "A(" + std::to_string(p.x) + ", " + std::to_string(p.y) + ")";
})
注1:注意lambda 表达式
的参数要与python中对应方法相匹配(别忘了self)
注2:类的static
方法或成员也可以直接定义为模块级,两种方式各有优劣:
- 定义到类里更有组织结构,后续扩展性更好
- 模块级别用起来更简单直接
2.2.4 重载函数
一般来说,重载函数的参数列表不一致,可用以下两种方式进行绑定:
static_cast
:通用解决方式.def("add", static_cast<int (A::*)(int, int)>(&A::add)) .def("add", static_cast<double (A::*)(double, double)>(&A::add))
lambda 表达式
:通过指定表达式的参数列表,达到重载的目的。对于复杂情况提供更灵活的处理.def("add", [](A& self, int a, int b) { // check sanity for a/b here ... return self.add(a, b) })
如果出错,可以使用签名宏进行检查:
// define signature checking macro
#define CHECK_SIGNATURE(func, expected) \
static_assert(std::is_same_v<decltype(func), expected>, "Signature mismatch for " #func)
// check function
CHECK_SIGNATURE(&A::xxx, double (A::*)(const A&) const);
2.2.5 模板函数
模板函数的绑定相对其他绑定来说有点麻烦了
因为它需要显式实例化模板后再绑定对应函数
// instantiate templates here
template int A::add<int>(int, int);
template double A::add<double>(double, double);
...
.def("add", static_cast<int (A::*)(int, int)>(&A::add))
.def("add", static_cast<double (A::*)(double, double)>(&A::add))
2.2.6 重载的运算符
除了上面绑定类方法的两种方式外,pybind11还提供非常方便的绑定运算符的方式
- pybind11支持方式:需添加头文件
<pybind11/operators.h>
.def(py::self + int())
- 调用C++类方式:
.def("__add__", &A::operator+)
lambda 表达式
方式:.def("__add__", [](A& self, A&other) { return self + other; })
注:因为操作符的返回值问题,有的是返回类对象本身,有的是返回新的对象,为跟C++类动作保持一致,可用return-value-policies指定返回值类型
- 返回本身:可用
py::return_value_policy::reference_internal
.def("__iadd__", &A::operator+=, py::return_value_policy::reference_internal)
- 返回新对象:默认自动处理。也可用
py::return_value_policy::automatic
.def("__add__", &A::operator+, py::return_value_policy::automatic)
python遇到二元操作符时一般会先调用前者的__xxx__
方法,不成功时再尝试调用后者的__rxxx__
方法,所以一般定义一边即可;另外,原地操作符一般是__ixxx__
常用 python magic method:
__neg__(self) // -self
__add__(self, other) // self + other
__sub__ // -
__mul__ // *
__truediv__ // /
__eq__ // ==
__ne__ // !=
__getitem__(self, key)
__setitem__(self, key, value)
2.2.7 友元函数
如下,类A有两个友元操作符:
// c++
class A {
public:
friend inline A operator+(const A& a, const A& b);
friend inline bool operator==(const A& a, const A& b);
};
可以用pybind11支持直接定义为类的成员
.def(py::self + py::self)
.def(py::self == py::self)
或者定义为模块级别的操作符:
m.def("__add__", [](const A& a, const A& b) {
return a + b;
});
m.def("__eq__", [](const A& a, const A& b) {
return a == b;
});
二、注意事项
1. CMakeLists.txt
检查事项:
- 仅使用pybind11头文件,是否添加了实现文件
- 有其他模块依赖时,是否链接了该模块
- 模块名是否一致:注意,由于windows大小写不敏感,所以不要让要生成的python模块与已有模块同名
- 检查想暴露的函数签名是否与预期一致
2. 多个类打包到同一个模块
上面已经说过,一个模块的定义只能有一次,所以如果一个模块有较多类需要绑定,则可以在各个类的绑定文件中定义绑定该类的函数,然后用一个单独的文件定义模块,并调用各个类的绑定函数:
假设有两个类A、B都需要绑定到同一个模块中:
- 总绑定文件 core_bindings.cpp
#include <pybind11/pybind11.h> void bindA(pybind11::module& m); void bindB(pybind11::module& m); PYBIND11_MODULE(your_module_name, m) { bindA(m); // call binding A class function bindB(m); // call binding B class function }
- 类绑定文件只需要实现对应绑定函数即可
// implement binding A class function void bindA(pybind11::module& m) { py::class_<A>... } // implement binding B class function void bindB(pybind11::module& m) { py::class_<B>... }
3. 处理一个类中对另一个类的引用
如果另一个类是作为参数或返回值类型(非继承关系),即只在方法内部调用,则可以不用管它也不用绑定
否则(有继承关系),该类必须要在本类之前进行绑定(直接在前面进行定义,如果在一个模块则编写它的绑定文件,不在一个模块则添加该模块的编译依赖),并且,在python中该模块需要在本模块之前进行导入(因为需要先有该类的定义)
4. 绑定派生类而不暴露基类
pybind11提供对这样绑定的支持,但是它需要在绑定时检查相关类的派生关系,这里涉及到访问权限的问题:
- 如果基类析构函数是
public
可直接在定义类时指明派生关系
或者声明但是不定义基类,然后使基类在python中不可见// B 继承自 A py::class_<B, A>(m, "B")
以上方式A类在python端均不可见,且// declare A py::class_<A> base_class(m, "A"); // B inherits from A py::class_<B, A>(m, "B")... // cover A m.attr("A") = py::none();
isinstance
不可用(因为没有A类) - 如果基类析构函数不是
public
,则编译时会提示"无法访问 protected 成员"
可自定义一个基类删除器,在声明基类时使用它,定义派生类后再隐藏基类
或者使用自定义跳板类,暴露基类析构函数,详见下节示例// define A class deleter struct ADeleter { void operator()(A* p) const { // access A's deconstructor through B delete static_cast<B*>(p); } }; PYBIND11_MODULE(...) { // declare A py::class_<A, std::unique_ptr<A, ADeleter>>(m, "A"); ... m.attr("A") = py::none(); }
5. 跳板类(Helper/Trampoline class)的利用
跳板类,就是我们自己定义继承自C++类的类,通过重写其中的方法达到改变访问权限、实例化虚基类等目的
5.1 暴露 protected/private 成员/方法
官方doc - binding protected member functions
- 暴露方法示例:
class PyA : public A { public: using A::foo; // A's foo is protected }; ... py::class_<A>(m, "A") .def("foo", &PyA::foo); // bind PyA's function
- 暴露析构函数示例:
class PyA : public A { public: using A::A; // using A's constructor ~PyA() override = default; // public deconstructor }; PYBIND11_MODULE(...) { // declare A py::class_<A, PyA>(m, "A"); ... }
5.2 绑定抽象基类方法
官方doc - overriding virtual functions
- 绑定已经有实现的虚函数:可以用上面类方法的方式直接声明基类并用
.def("", &...)
进行绑定 - 绑定无实现的虚函数/纯虚函数,或想要支持 python 重写的函数:必须用跳板类方法,且用
PYBIND11_OVERRIDE(_PURE)
声明进行转发调用(加_PURE
为纯虚函数)
注1:定义跳板类后,绑定基类时class PyA : public A { public: using A::A; // override pure virtual function in A void xxx() override { PYBIND11_OVERRIDE_PURE( void, // return type A, // base class type xxx // function name ); } }; ... py::class_<A, PyA>(m, "A") .def("xxx", &A::xxx) // we still bind A's function (not PyA's)
.def()
中仍是写基类的方法
注2:此方法也适用于绑定工厂函数等需要多态支持的场景m.def("createB", []() { return new B(); });