pybind11学习 | 面向对象编程
本文主要记录官方文档中 OBJECT-ORIENTED CODE 一章的学习笔记。
文章目录
- 1 自定义类的绑定
- 2 绑定匿名函数
- 3 成员变量
- 4 动态属性
- 5 继承与向下转型
- 6 重载方法
- 7 枚举类型
- 8 总结
- 参考
1 自定义类的绑定
在C++中自定义一个数据结构Pet,代码如下:
struct Pet {
Pet(const std::string &name) : name(name) { }
void setName(const std::string &name_) { name = name_; }
const std::string &getName() const { return name; }
std::string name;
};
绑定代码如下:
#include <pybind11/pybind11.h>
namespace py = pybind11;
PYBIND11_MODULE(example, m) {
py::class_<Pet>(m, "Pet")
.def(py::init<const std::string &>())
.def("setName", &Pet::setName)
.def("getName", &Pet::getName);
}
py::class_
用于创建C++ class或 struct的绑定(对应到Python中就是class)。py::class_<Pet>(m, "Pet")
实例化模板,尖括号中是需绑定的class或struct,第一个参数是模块变量,用于指定Python class所在的模块;第二个参数是Python class的名称。py::class_::def
用于绑定类的成员函数。py::init()
方法使用类构造函数的参数类型作为模板参数,并包装相应的构造函数。静态成员函数需要使用py::class_::def_static
来绑定。
Python使用:
>>> import example
>>> example.Pet
<class 'example.Pet'>
>>> cat=example.Pet("eric")
>>> cat
<example.Pet object at 0x0000023DB7E705F0>
>>> cat.getName()
'eric'
>>> cat.setName("bob")
>>> cat.getName()
'bob'
2 绑定匿名函数
为了返回具有可读性的对象信息,我们绑定一个函数到__repr__
方法,为了方便我们可以使用匿名函数。
py::class_<Pet>(m, "Pet")
.def(py::init<const std::string &>())
.def("setName", &Pet::setName)
.def("getName", &Pet::getName)
.def("__repr__",
[](const Pet &a) {
return "<example.Pet named '" + a.name + "'>";
});
修改前:
>>> print(cat)
<example.Pet object at 0x0000023DB7E705F0>
修改后:
>>> print(cat)
<example.Pet named 'eric'>
3 成员变量
使用class_::def_readwrite
方法可以导出公有成员变量,使用class_::def_readonly
方法则可以导出只读成员变量。
py::class_<Pet>(m, "Pet")
.def(py::init<const std::string &>())
.def_readwrite("name", &Pet::name)
// ... remainder ...
Python中使用示例如下:
>>> import example
>>> cat = example.Pet("eric")
>>> cat.name
'eric'
>>> cat.name = 'bob'
>>> cat.name
'bob'
假设Pet::name
是一个私有成员变量,向外提供setters
(修改变量值)和getters
(获取变量值)方法。
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_::def_property()
(只读成员使用class_::def_property_readonly()
)来导出私有成员,并生成相应的setter
和getter
方法:
py::class_<Pet>(m, "Pet")
.def(py::init<const std::string &>())
.def_property("name", &Pet::getName, &Pet::setName)
// ... remainder ...
Python测试:
>>> import example
>>> example.Pet.name
<property object at 0x000001F7F7B097C0>
>>> cat = example.Pet("eric")
>>> cat.getName()
'eric'
>>> cat.name
'eric'
>>> cat.name = 'bob'
>>> cat.name
'bob'
只写属性通过将read函数定义为nullptr来实现。
class_::def_readwrite_static()
,class_::def_readonly_static()
class_::def_property_static()
,class_::def_property_readonly_static()
用于绑定静态变量和属性。
4 动态属性
我们知道,Python原生类可以动态获得新属性。
>>> class Pet:
... name = "Molly"
...
>>> p = Pet()
>>> p.name = "Charly" # overwrite existing
>>> p.age = 2 # dynamically add a new attribute
默认情况下,从C++导出的类不支持动态属性,其可写属性必须是通过class_::def_readwrite
或class_::def_property
定义的。试图设置其他属性将产生错误。为了让C++导出的类也支持动态属性,我们需要在py::class_
的构造函数中添加py::dynamic_attr
标识:
py::class_<Pet>(m, "Pet", py::dynamic_attr())
.def(py::init<const std::string &>())
.def_readwrite("name", &Pet::name);
// ... remainder ...
这样就可以在为导出的类添加动态属性了,Python中测试如下:
>>> import example
>>> cat = example.Pet("eric")
>>> cat.name
'eric'
>>> cat.age = 12
>>> cat.age
12
>>> cat.__dict__
{'age': 12}
5 继承与向下转型
现在有两个具有继承关系的类:
struct Pet {
Pet(const std::string &name) : name(name) { }
std::string name;
};
struct Dog : Pet {
Dog(const std::string &name) : Pet(name) { }
std::string bark() const { return "woof!"; }
};
pybind11提供了两种方法来指明继承关系:1)将C++基类作为派生类class_
的模板参数;2)将基类名作为class_
的参数绑定到派生类。两种方法是等效的。
py::class_<Pet>(m, "Pet")
.def(py::init<const std::string &>())
.def_readwrite("name", &Pet::name);
// Method 1: template parameter:
py::class_<Dog, Pet /* <- specify C++ parent type */>(m, "Dog")
.def(py::init<const std::string &>())
.def("bark", &Dog::bark);
py::class_<Pet> pet(m, "Pet");
pet.def(py::init<const std::string &>())
.def_readwrite("name", &Pet::name);
// Method 2: pass parent class_ object:
py::class_<Dog>(m, "Dog", pet /* <- specify Python parent type */)
.def(py::init<const std::string &>())
.def("bark", &Dog::bark);
指明继承关系后,派生类实例将获得两者的属性和方法:
>>> p = example.Dog("Molly")
>>> p.name
'Molly'
>>> p.bark()
'woof!'
上面的例子是一个常规非多态的继承关系,表现在Python就是:
// 返回一个指向派生类的基类指针
m.def("pet_store", []() { return std::unique_ptr<Pet>(new Dog("Molly")); });
>>> p = example.pet_store()
>>> type(p) # `Dog` instance behind `Pet` pointer
Pet # no pointer downcasting for regular non-polymorphic types
>>> p.bark()
AttributeError: 'Pet' object has no attribute 'bark'
pet_store
函数返回了一个Dog实例,但由于基类并非多态类型,Python只识别到了Pet。在C++中,一个类至少有一个虚函数才会被视为多态类型。pybind11会自动识别这种多态机制。
struct PolymorphicPet {
virtual ~PolymorphicPet() = default;
};
struct PolymorphicDog : PolymorphicPet {
std::string bark() const { return "woof!"; }
};
// Same binding code
py::class_<PolymorphicPet>(m, "PolymorphicPet");
py::class_<PolymorphicDog, PolymorphicPet>(m, "PolymorphicDog")
.def(py::init<>())
.def("bark", &PolymorphicDog::bark);
// Again, return a base pointer to a derived instance
m.def("pet_store2", []() { return std::unique_ptr<PolymorphicPet>(new PolymorphicDog); });
>>> p = example.pet_store2()
>>> type(p)
PolymorphicDog # automatically downcast
>>> p.bark()
u'woof!'
pybind11会自动地将一个指向多态基类的指针,向下转型为实际的派生类类型。这和C++常见的情况不同,我们不仅可以访问基类的虚函数,还能访问派生类的方法和属性。
6 重载方法
重载方法即拥有相同的函数名,但传入参数不一样的函数:
struct Pet {
Pet(const std::string &name, int age) : name(name), age(age) { }
void set(int age_) { age = age_; }
void set(const std::string &name_) { name = name_; }
std::string name;
int age;
};
我们在绑定Pet::set
时会报错,因为编译器并不知道用户想选择哪个重载方法。我们需要添加具体的函数指针来消除歧义。绑定多个函数到同一个Python名称,将会自动创建函数重载链。Python将会依次匹配,找到最合适的重载函数。
py::class_<Pet>(m, "Pet")
.def(py::init<const std::string &, int>())
.def("set", static_cast<void (Pet::*)(int)>(&Pet::set), "Set the pet's age")
.def("set", static_cast<void (Pet::*)(const std::string &)>(&Pet::set), "Set the pet's name");
如果编译器支持C++14,也可以使用下面的语法来转换重载函数:
py::class_<Pet>(m, "Pet")
.def("set", py::overload_cast<int>(&Pet::set), "Set the pet's age")
.def("set", py::overload_cast<const std::string &>(&Pet::set), "Set the pet's name");
这里,py::overload_cast
仅需指定函数类型,不用给出返回值类型,以避免原语法带来的不必要的干扰(void (Pet::*)
)。如果是基于const的重载,需要使用py::const_
标识。
struct Widget {
int foo(int x, float y);
int foo(int x, float y) const;
};
py::class_<Widget>(m, "Widget")
.def("foo_mutable", py::overload_cast<int, float>(&Widget::foo))
.def("foo_const", py::overload_cast<int, float>(&Widget::foo, py::const_));
如果想在仅支持C++11的编译器上使用py::overload_cast
语法,可以使用py::detail::overload_cast_impl
来代替:
template <typename... Args>
using overload_cast_ = pybind11::detail::overload_cast_impl<Args...>;
py::class_<Pet>(m, "Pet")
.def("set", overload_cast_<int>()(&Pet::set), "Set the pet's age")
.def("set", overload_cast_<const std::string &>()(&Pet::set), "Set the pet's name");
Note: 如果想定义多个重载的构造函数,使用
.def(py::init<...>())
语法依次定义就好,指定关键字和默认参数的机制也还是生效的。
7 枚举类型
现在有一个含有枚举和内部类型的类:
struct Pet {
enum Kind {
Dog = 0,
Cat
};
struct Attributes {
float age = 0;
};
Pet(const std::string &name, Kind type) : name(name), type(type) { }
std::string name;
Kind type;
Attributes attr;
};
绑定代码如下所示:
py::class_<Pet> pet(m, "Pet");
pet.def(py::init<const std::string &, Pet::Kind>())
.def_readwrite("name", &Pet::name)
.def_readwrite("type", &Pet::type)
.def_readwrite("attr", &Pet::attr);
py::enum_<Pet::Kind>(pet, "Kind")
.value("Dog", Pet::Kind::Dog)
.value("Cat", Pet::Kind::Cat)
.export_values();
py::class_<Pet::Attributes>(pet, "Attributes")
.def(py::init<>())
.def_readwrite("age", &Pet::Attributes::age);
为确保嵌套类型Kind
和Attributes
在Pet
的作用域中创建,我们必须向enum_
和class_
的构造函数提供class_
实例pet
。enum_::export_values()
用来导出枚举项到父作用域,C++11的强枚举类型需要跳过这点。
>>> p = Pet("Lucy", Pet.Cat)
>>> p.type
Kind.Cat
>>> int(p.type)
1
枚举类型的枚举项会被导出到类的__members__
属性中:
>>> Pet.Kind.__members__
{'Dog': Kind.Dog, 'Cat': Kind.Cat}
name
属性可以返回枚举值的名称的unicode字符串,str(enum)
也可以做到,但两者的实现目标不同。下面的例子展示了两者的差异:
>>> p = Pet("Lucy", Pet.Cat)
>>> pet_type = p.type
>>> pet_type
Pet.Cat
>>> str(pet_type)
'Pet.Cat'
>>> pet_type.name
'Cat'
Note: 当我们给
enum_
的构造函数增加py::arithmetic()
标识时,pybind11将创建一个支持基本算术运算和位运算(如比较、或、异或、取反等)的枚举类型。py::enum_<Pet::Kind>(pet, "Kind", py::arithmetic()) ...
默认情况下,省略这些可以节省内存空间。
8 总结
- 默认情况下,pybind11导出的类比原生Python类效率更高。
参考
[1] 官方文档:pybind11 documentation
[2] 官方文档中文翻译:pybind11-Chinese-docs: pybind11中文文档