Pybind11 简介
#头条创作挑战赛#
Pybind11 通过 C++ 编译时来推断类型信息,来最大程度地减少传统扩展Python 模块是繁杂的模板代码,且实现了常见数据类型,如 STL 数据结构、智能指针、类、函数重载、到 Python 的自动转换,其中函数可以接收和返回自定义数据类型的值、指针或引用。
特点:
- 轻量且功能单一,聚焦于提供 C++ & Python binding,交互代码简洁
- 对常见的 C++数据类型如 STL、Python 库如 numpy 等兼容很好,无人工转换成本
- only header 方式,无需额外代码生成,编译期即可完成绑定关系建立,减小 binary 大小
- 支持 C++新特性,对 C++的重载、继承,debug 方式便捷易用
使用pybind11来绑定普通函数
1,也就是使用pybind11来绑定一个简单的函数:
//example.cpp
#include <pybind11/pybind11.h>
int add(int i, int j) {
return i + j;
}
PYBIND11_MODULE(example, m) {
m.doc() = "pybind11 example plugin"; // optional module docstring
m.def("add", &add, "A function which adds two numbers");
}
运行:
>>> import example
>>> example.add(3,4)
7
2,使用pybind11::arg来定义keyword参数:
m.def("add1", &add, pybind11::arg("i"), pybind11::arg("j"), "add two numbers");
这个时候就可以使用的调用形式了。
>>> import example
>>> add1(i=70,j=30)
100
3,使用默认参数:
m.def("add2", &add, pybind11::arg("i") = 1, pybind11::arg("j") = 2);
使用pybind11来绑定class
和普通的函数绑定相比,绑定class的时候由m.def转变为了pybind11::class_<class>.def了;另外,还需要显式的指定class的构造函数的参数类型。
1,一个最简单的c++类的pybind绑定
class Pet {
public:Pet (const std::string &name) : name_(name) { }
void setName(const std::string &name) { name_ = name; }
const std::string &getName() const { return name_; }
private:std::string name_;
};
PYBIND11_MODULE(example, m)
{
pybind11::class_<Pet>(m, "Pet")
.def(pybind11::init<const std::string &>())
.def("setName", &Pet::setName)
.def("getName", &Pet::getName);
}
运行:
>>> import example
>>> dir(example)
['Pet', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__']
>>> x = example.Pet('Molly')
>>> x.getName
<bound method PyCapsule.getName of <example.Pet object at 0x7f4be3daedb0>>
>>> x.getName()
'Molly'
>>> x.setName('Charly')
>>> x.getName()
'Charly'
2,模拟python风格的property
在上面的class绑定的例子中,我们并没有办法来像python中访问property的方式来访问name_私有成员,比如print(x.name_)这样。不过pybind11提供了def_property的接口:
m.def_property("name_", &Pet::getName, &Pet::setName);
通过def_property的定义,我们就可以像访问python的property风格那样访问name_。运行如下:
>>> import example
>>> x = example.Pet("Molly")
>>> x.getName()
'Molly'
>>> x.name_
'Molly'
>>> x.name_ = 'Charly'
>>> x.getName()
'Charly'
>>>
类似的还有class_::def_readwrite() 、class_::def_readonly()等定义。但是这样都是在已经定义好的类成员上进行读写,而python中的对象上还可以增加动态属性,就是一个class中本没有这个成员,但是直接赋值后也就产生了......这就是动态属性,比如上面的x.name_虽然是可以按照python风格来读写了,但是你要是直接赋值给一个x.age是不行的(我们并没有定义age成员)。那怎么办呢?
使用pybind11::dynamic_attr(),代码如下所示:
PYBIND11_MODULE(example, m) {
pybind11::class_<Pet>(m, "Pet",pybind11::dynamic_attr())
.def(pybind11::init<const std::string &>())
.def("setName", &Pet::setName)
.def("getName", &Pet::getName)
.def_property("name_", &Pet::getName, &Pet::setName);
}
运行如下:
>>> import example
>>> x = example.Pet("Molly")
>>> x.age =18
>>> x.age
18
>>> x.gender
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'example.Pet' object has no attribute 'gender'
>>> x.__dict__
{'age': 18}
3,继承关系的Python绑定
设想这个时候,我们引入了继承关系,定义类Dog继承自类Pet:
class Pet{
public:Pet(const std::string &name) : name_(name) { }
void setName(const std::string &name) { name_ = name; }
const std::string &getName() const { return name_; }
private:std::string name_;
};
class Dog: public Pet{
public:Dog(const std::string& name, int age) : Pet(name),age_(age){}
const int getAge() const {return age_; }
void setAge(int age) {age_ = age;}
private:int age_;
};
这个时候怎么绑定Dog类到python呢?方法就是模拟python风格,在括号里应用父类的python对象:
PYBIND11_MODULE(example, m) {
pybind11::class_<Pet>(m, "Pet");
Pet.def(pybind11::init<const std::string &>())
.def("setName", &Pet::setName)
.def("getName", &Pet::getName)
.def_property("name_", &Pet::getName, &Pet::setName);
pybind11::class_<Dog,Pet>(m, "Dog")
.def(pybind11::init<const std::string &, int>())
.def("setAge", &Dog::setAge)
.def("getAge", &Dog::getAge)
.def_property("age_", &Dog::getAge, &Dog::setAge);
}
4,多态的Python绑定
当你在模块里新增一个工厂方法(只是举例)来返回一个Dog的实例:
m.def("create", []() { return std::unique_ptr<Pet>(new Dog("Molly", 18)); });
你会发现虽然new的是Dog实例,但是该实例是隐藏在Pet指针后的,因此这种create方法得到的实例只能使用Pet成员,而不能使用Dog新扩展出来的成员。用python验证下也会发现这种问题:
>>> import example
>>> x = example.create()
>>> type(x)
<class 'example.Pet'>
>>> x.getName()
'Molly'
>>> x.getAge()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>AttributeError: 'example.Pet' object has no attribute 'getAge'
>>>
要改变这种情况,需要在基类中添加至少一个virtual函数,这样pybind11就自动认识到当前的情况了。比如像下面这样改动,增加虚析构后:
class Pet{
......
virtual ~Pet() = default;
......
};
class Dog: public Dog{
......
};
就增加一个virtual函数,其它python绑定函数不变,这个时候,create的实例就代表Dog了:
>>> import example
>>> x = example.create()
>>> type(x)
<class 'example.Dog'>
>>> x.getAge()
18
5,函数重载的Python绑定
大多数情况下,C++的代码中都会出现函数重载的情况,就像下面所示的代码:
class Dog: public Pet{
public:......
void setAge(int age) {age_ = age;}
void setAge(const std::string& age){ages_ = age;}
private:int age_;
std::string ages_;
};
这个时候如果还像之前那样绑定的话,就会出现unresolved overloaded function type的错误。pybind11当然提供了对应的解决方案,不止一种,这里展示更优雅的C++14的解决方案,使用pybind11::overload_cast:
pybind11::class_<Dog,Pet>(m, "Dog")
.def(pybind11::init<const std::string &, int>())
.def("setAge", pybind11::overload_cast<int>(&Dog::setAge))
.def("getAge", &Dog::getAge)
.def_property("age_", &Dog::getAge, pybind11::overload_cast<int>(&Dog::setAge));
6,对enum的绑定
使用pybind11::enum_,如下所示:
py::enum_<Pet::Kind>(pet, "Kind")
.value("Dog", Pet::Kind::Dog)
.value("Cat", Pet::Kind::Cat)
.export_values();
enum_::export_values() 会将这个枚举类型所有的值都导出到parent对象的作用域,上面这个例子就会导出到sys的作用域,所以视情况添加。
pybind11对C++异常的处理
当在Python中调用pybind11绑定的C++代码时,如果这部分C++代码抛出了异常,就像下面这样:
class Dog: public Pet{
public:Dog(const std::string& name, int age) : Pet(name),age_(age){}
const int getAge() const {throw std::runtime_error("I,AM,Dog"); return age_; }
void setAge(int age) {age_ = age;}
void setAge(std::string age){ages_ = age;}
private:int age_;
std::string ages_;
};
还是上文那个Dog类,在getAge函数里我手动设置抛出了runtime_error异常,那么在Python中调用这个函数时,Pybind11会将这些C++的异常转换为对应的Python异常:
>>> import example
>>> x=example.create()
>>> x.getAge()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
RuntimeError: I,AM,Dog
>>> try:
... x.getAge()
... except Exception as e:
... print(type(e))
...
<class 'RuntimeError'>
>>>
pybind11定义了C++中std::exception(及其子类)到Python异常的转换关系:
Exception thrown by C++ | Translated to Python exception type |
std::exception | RuntimeError |
std::bad_alloc | MemoryError |
std::domain_error | ValueError |
std::invalid_argument | ValueError |
std::length_error | ValueError |
std::out_of_range | IndexError |
std::range_error | ValueError |
std::overflow_error | OverflowError |
pybind11::stop_iteration | StopIteration (used to implement custom iterators) |
pybind11::index_error | IndexError (used to indicate out of bounds access in __getitem__, __setitem__, etc.) |
pybind11::key_error | KeyError (used to indicate out of bounds access in __getitem__, __setitem__ in dict-like objects, etc.) |
pybind11::value_error | ValueError (used to indicate wrong value passed in container.remove(...)) |
pybind11::type_error | TypeError |
pybind11::buffer_error | BufferError |
pybind11::import_error | ImportError |
pybind11::attribute_error | AttributeError |
Any other exception | RuntimeError |
对于那些自定义的C++异常,你就需要使用pybind11::register_exception了;
C++和Python之间的类型转换
pybind11提供了三种基本的机制来实现C++和Python之间的类型转换,前两种就是个wrapper,最后一种才是真正的类型转换(要有内存的拷贝):
1,类型定义在C++中,而在Python中访问
假设定义了一个C++类型Dog,pybind11生成Dog的wrapper,该wrapper又符合Python的内存布局规范,从而python代码可以访问Dog;也就是pybind11封装C++类型来兼容Python。
比如,对于前述章节的pybind11::class_封装的Dog类,Dog类生命周期内都是一个C++的类型,而通过这种wrapper提供了一个Python的接口。当一个Dog的对象从C++来到Python时,pybind11会自动在这个对象上加上一个wrapper;当从Python返回到C++的时候,再把这个wrapper去掉。
2,类型定义在Python中,而在C++中访问
这和第一种情况相反,这种情况下我们假设有一个Python的类型,比如tuple或者list,然后我们使用pybind11::object系列的wrapper来封装tuple、list或者其它,从而得以在C++中访问。比如下面这个例子:
void print_list(pybind11::list my_list) {
for (auto item : my_list)
std::cout << item << " ";
}
然后在python中:
>>> print_list([7, 0, 3, 0])
7 0 3 0
在这个调用中,Python的list并没有被本质的转换,只不过被wrap在了C++的pybind11::list类型中。像list这样支持直接被wrap的python类型还有
- handle
- object
- bool_
- int_
- float_
- str
- bytes
- tuple
- list
- dict
- slice
- none
- capsule
- iterable
- iterator
- function
- buffer
- array
- array_t
3,C++和Python之间的类型转换
前两种情况下,我们都是在C++或者Python中有一个native的类型,然后通过wrapper来互相访问。在第三种情况下,我们在C++和Python中都使用自身native的类型——然后尝试通过复制内存的方式来转换它们。比如,在C++中定义函数:
void print_vector(const std::vector<int> &v) {
for (auto item : v)
std::cout << item << "\n";
}
在python中运行:
print_vector([1, 2, 3])
>>> 1 2 3
pybind11构造了一个全新的std::vector<int>对象,然后将python list中的每个元素拷贝了过来——然后这个新构造的std::vector<int>对象再传递给print_vector函数。pybind11默认支持很多种这样的类型转换,虽然比较方便,比如上面是python list到c++ vector,也可以是Python tuple到c++ vector。到底有多少种类型支持默认转换,如下所示:
- int8_t, uint8_t
- int16_t, uint16_t
- int32_t, uint32_t
- int64_t, uint64_t
- ssize_t, size_t
- float, double
- bool
- char
- char16_t
- char32_t
- wchar_t
- const char *
- const char16_t *
- const char32_t *
- const wchar_t *
- std::string
- std::u16string
- std::u32string
- std::wstring
- std::string_view, std::u16string_view, etc.
- std::pair<T1, T2>
- std::tuple<...>
- std::reference_wrapper<...>
- std::complex<T>
- std::array<T, Size>
- std::vector<T>
- std::deque<T>
- std::valarray<T>
- std::list<T>
- std::map<T1, T2>
- std::unordered_map<T1, T2>
- std::set<T>
- std::unordered_set<T>
- std::optional<T>
- std::experimental::optional<T>
- std::variant<...>
- std::function<...>
- std::chrono::duration<...>
- std::chrono::time_point<...>
- Eigen::Matrix<...>
- Eigen::Map<...>
- Eigen::SparseMatrix<...>
总结起来就是基础类型、容器类型、std::function、std::chrono、Eigen。但是要注意这种转换中间引入了完完全全的内存拷贝!小的类型还可以,但是对于大内存的类型就非常不友好。还有,这其中有些转换虽然是自动的,但是需要你手工添加相应的pybind11头文件,不然会在运行时报错:
Did you forget to `#include <pybind11/stl.h>`? Or <pybind11/complex.h>,
<pybind11/functional.h>, <pybind11/chrono.h>, etc. Some automatic
conversions are optional and require extra headers to be included
when compiling your pybind11 module.
函数返回值和传参时候的内存处理
让我们来见识下一个简单的函数绑定的例子:
struct Data
{
int value;
std::string name;
};
Data _data = {1, "test"};
Data *get_data() { return &_data; }
py::class_<Data>(m, "Data");
m.def("get_data", &get_data, " A function which get data");
很简单的一个pybind11绑定。然后运行,意想不到的事情发生了:
>>> import example
>>> example.getData()
<example.getDataobject at 0x7f3af3951bf0>
>>>
free(): invalid pointer
Aborted (core dumped)
看到上面退出python解释器时的Aborted (core dumped)的crash悲剧了吗???谁能说说发生了什么???
Python和C++使用了截然不同的内存管理和对象生命周期管理。因此,pybind11提供了好几种函数返回值的内存拷贝策略,而默认策略是return_value_policy::automatic。在上面的例子中,当get_data()从python中返回时,返回类型Data被wrap为一个Python类型,默认的内存拷贝策略是return_value_policy::automatic,这就导致python的wrapper认为自己是&Data这块内存的主人(这块内存是在static区域)。当结束python会话时,Python的内存管理器会删除Python wrapper——然后删除其管理的内存——然后C++部分也会销毁Data对象的内存——这就导致了双重delete——程序crash了。
为了解决上述的问题,我们就不应该使用默认的内存拷贝策略,而是使用reference策略:
m.def("get_data", &get_data, " A function which get data", py::return_value_policy::reference);
这样的话全局的Data内存就会被reference而避免了被双重delete的命运。
在pybind11中,一共有如下这么多的内存拷贝策略:
Return value policy | Description |
return_value_policy::take_ownership | Reference an existing object (i.e. do not create a new copy) and take ownership. Python will call the destructor and delete operator when the object’s reference count reaches zero. Undefined behavior ensues when the C++ side does the same, or when the data was not dynamically allocated. |
return_value_policy::copy | Create a new copy of the returned object, which will be owned by Python. This policy is comparably safe because the lifetimes of the two instances are decoupled. |
return_value_policy::move | Use std::move to move the return value contents into a new instance that will be owned by Python. This policy is comparably safe because the lifetimes of the two instances (move source and destination) are decoupled. |
return_value_policy::reference | Reference an existing object, but do not take ownership. The C++ side is responsible for managing the object’s lifetime and deallocating it when it is no longer used. Warning: undefined behavior will ensue when the C++ side deletes an object that is still referenced and used by Python. |
return_value_policy::reference_internal | Indicates that the lifetime of the return value is tied to the lifetime of a parent object, namely the implicit this, or self argument of the called method or property. Internally, this policy works just like return_value_policy::reference but additionally applies a keep_alive<0, 1> call policy (described in the next section) that prevents the parent object from being garbage collected as long as the return value is referenced by Python. This is the default policy for property getters created via def_property, def_readwrite, etc. |
return_value_policy::automatic | This policy falls back to the policy return_value_policy::take_ownership when the return value is a pointer. Otherwise, it uses return_value_policy::move or return_value_policy::copy for rvalue and lvalue references, respectively. See above for a description of what all of these different policies do. This is the default policy for py::class_-wrapped types. |
return_value_policy::automatic_reference | As above, but use policy return_value_policy::reference when the return value is a pointer. This is the default conversion policy for function arguments when calling Python functions manually from C++ code (i.e. via handle::operator()) and the casters in pybind11/stl.h. You probably won’t need to use this explicitly. |
将Python解释器集成到C++程序中
pybind11的目的主要是使得用户可以方便的在python中调用C++模块。然而它也可以帮助用户将python解释器集成到C++程序中,这样我们的C++程序就可以像调用函数一样调用py模块了。这种情况下,你的CMakeLists.txt就不能继续使用pybind11_add_module了,反过来,应该是link一个pybind11::embed目标。
#include <pybind11/embed.h> // everything needed for embedding
int main() {
pybind11::scoped_interpreter guard{}; // start the interpreter and keep it alive
pybind11::print("Hello, World!"); // use the Python API
}
很明显的就是头文件换成embed.h了。然后,CMakeLists.txt需要做出改变,像上面说的,不是要做一个python插件了,而是要把python解释器集成到C++程序中。
main函数其中的pybind11::scoped_interpreter初始化的时候,也正是python解释器生命周期开始的时候;当pybind11::scoped_interpreter对象销毁的时候,也正是python解释器生命周期结束的时候。而在这之后,如果再次实例化了pybind11::scoped_interpreter对象,则python解释器的生命周期又会重新开始。
Adding embedded modules
Embedded binary modules can be added using the PYBIND11_EMBEDDED_MODULE macro. Note that the definition must be placed at global scope. They can be imported like any other module.
#include <pybind11/embed.h>
namespace py = pybind11;
PYBIND11_EMBEDDED_MODULE(fast_calc, m) {
m.def("add", [](int i, int j) {
return i + j;
});
}
注意事项
?? 头文件包含问题
因为“python.h”存在宏定义slots,与QT冲突,所以尽量避免同时包含python.h 和其他qt相关头文件,如果一定需要,请将python相关头文件放在前面