本文主要记录官方文档中 **OBJECT-ORIENTED CODE **一章的学习笔记。
[TOC]
1 自定义类的绑定 在C++中自定义一个数据结构Pet,代码如下:
1 2 3 4 5 6 7 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; };
绑定代码如下:
1 2 3 4 5 6 7 8 9 #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使用:
1 2 3 4 5 6 7 8 9 10 11 >>> 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__
方法,为了方便我们可以使用匿名函数。
1 2 3 4 5 6 7 8 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 + "'>" ; });
修改前:
1 2 >>> print (cat) <example.Pet object at 0x0000023DB7E705F0 >
修改后:
1 2 >>> print (cat) <example.Pet named 'eric' >
3 成员变量 使用class_::def_readwrite
方法可以导出公有成员变量,使用class_::def_readonly
方法则可以导出只读成员变量。
1 2 3 4 py::class_<Pet>(m, "Pet" ) .def (py::init<const std::string &>()) .def_readwrite ("name" , &Pet::name)
Python中使用示例如下:
1 2 3 4 5 6 7 >>> import example>>> cat = example.Pet("eric" )>>> cat.name'eric' >>> cat.name = 'bob' >>> cat.name'bob'
假设Pet::name
是一个私有成员变量,向外提供setters
(修改变量值)和getters
(获取变量值)方法。
1 2 3 4 5 6 7 8 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
方法:
1 2 3 4 py::class_<Pet>(m, "Pet" ) .def (py::init<const std::string &>()) .def_property ("name" , &Pet::getName, &Pet::setName)
Python测试:
1 2 3 4 5 6 7 8 9 10 11 >>> 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原生类可以动态获得新属性。
1 2 3 4 5 6 >>> class Pet :... name = "Molly" ...>>> p = Pet()>>> p.name = "Charly" >>> p.age = 2
默认情况下,从C++导出的类不支持动态属性,其可写属性必须是通过class_::def_readwrite
或class_::def_property
定义的。试图设置其他属性将产生错误。为了让C++导出的类也支持动态属性,我们需要在py::class_
的构造函数中添加py::dynamic_attr
标识:
1 2 3 4 py::class_<Pet>(m, "Pet" , py::dynamic_attr ()) .def (py::init<const std::string &>()) .def_readwrite ("name" , &Pet::name);
这样就可以在为导出的类添加动态属性了,Python中测试如下:
1 2 3 4 5 6 7 8 9 >>> import example>>> cat = example.Pet("eric" )>>> cat.name 'eric' >>> cat.age = 12 >>> cat.age12 >>> cat.__dict__ {'age' : 12 }
5 继承与向下转型 现在有两个具有继承关系的类:
1 2 3 4 5 6 7 8 9 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_
的参数绑定到派生类。两种方法是等效的。
1 2 3 4 5 6 7 8 py::class_<Pet>(m, "Pet" ) .def (py::init<const std::string &>()) .def_readwrite ("name" , &Pet::name); py::class_<Dog, Pet >(m, "Dog" ) .def (py::init<const std::string &>()) .def ("bark" , &Dog::bark);
1 2 3 4 5 6 7 8 py::class_<Pet> pet (m, "Pet" ) ; pet.def (py::init<const std::string &>()) .def_readwrite ("name" , &Pet::name); py::class_<Dog>(m, "Dog" , pet ) .def (py::init<const std::string &>()) .def ("bark" , &Dog::bark);
指明继承关系后,派生类实例将获得两者的属性和方法:
1 2 3 4 5 >>> p = example.Dog("Molly" )>>> p.name'Molly' >>> p.bark()'woof!'
上面的例子是一个常规非多态的继承关系,表现在Python就是:
1 2 m.def ("pet_store" , []() { return std::unique_ptr<Pet>(new Dog ("Molly" )); });
1 2 3 4 5 >>> p = example.pet_store()>>> type (p) Pet >>> p.bark() AttributeError: 'Pet' object has no attribute 'bark'
pet_store
函数返回了一个Dog实例,但由于基类并非多态类型,Python只识别到了Pet。在C++中,一个类至少有一个虚函数 才会被视为多态类型 。pybind11会自动识别这种多态机制 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 struct PolymorphicPet { virtual ~PolymorphicPet () = default ; };struct PolymorphicDog : PolymorphicPet { std::string bark () const { return "woof!" ; } }; py::class_<PolymorphicPet>(m, "PolymorphicPet" ); py::class_<PolymorphicDog, PolymorphicPet>(m, "PolymorphicDog" ) .def (py::init<>()) .def ("bark" , &PolymorphicDog::bark); m.def ("pet_store2" , []() { return std::unique_ptr<PolymorphicPet>(new PolymorphicDog); });
1 2 3 4 5 >>> p = example.pet_store2()>>> type (p) PolymorphicDog >>> p.bark()u'woof!'
pybind11会自动地将一个指向多态基类的指针,向下转型 为实际的派生类类型。这和C++常见的情况不同,我们不仅可以访问基类的虚函数,还能访问派生类的方法和属性。
6 重载方法 重载方法即拥有相同的函数名,但传入参数不一样的函数:
1 2 3 4 5 6 7 8 9 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将会依次匹配,找到最合适的重载函数。
1 2 3 4 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,也可以使用下面的语法来转换重载函数:
1 2 3 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_
标识。
1 2 3 4 5 6 7 8 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
来代替:
1 2 3 4 5 6 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 枚举类型 现在有一个含有枚举和内部类型的类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 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; };
绑定代码如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 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的强枚举类型需要跳过这点。
1 2 3 4 5 >>> p = Pet("Lucy" , Pet.Cat)>>> p.type Kind.Cat>>> int (p.type )1
枚举类型的枚举项会被导出到类的__members__
属性中:
1 2 >>> Pet.Kind.__members__ {'Dog' : Kind.Dog, 'Cat' : Kind.Cat}
name
属性可以返回枚举值的名称的unicode字符串,str(enum)
也可以做到,但两者的实现目标不同。下面的例子展示了两者的差异:
1 2 3 4 5 6 7 8 >>> 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将创建一个支持基本算术运算和位运算(如比较、或、异或、取反等)的枚举类型。
1 2 py::enum_<Pet::Kind>(pet, "Kind" , py::arithmetic ()) ...
默认情况下,省略这些可以节省内存空间。
8 总结
默认情况下,pybind11导出的类比原生Python类效率更高。
参考 [1] 官方文档:pybind11 documentation
[2] 官方文档中文翻译:pybind11-Chinese-docs: pybind11中文文档