5. 面对对象编程

5.1 创建一个自定义类的绑定

让我们来看一个更加复杂的例子:绑定一个C++自定义数据结构Pet。定义如下:

  1. struct Pet {
  2. Pet(const std::string &name) : name(name) { }
  3. void setName(const std::string &name_) { name = name_; }
  4. const std::string &getName() const { return name; }
  5. std::string name;
  6. };

绑定代码如下所示:

  1. #include <pybind11/pybind11.h>
  2. namespace py = pybind11;
  3. PYBIND11_MODULE(example, m) {
  4. py::class_<Pet>(m, "Pet")
  5. .def(py::init<const std::string &>())
  6. .def("setName", &Pet::setName)
  7. .def("getName", &Pet::getName);
  8. }

class_会创建C++ class或 struct的绑定。init()方法使用类构造函数的参数类型作为模板参数,并包装相应的构造函数(详见自定义构造函数)。Python使用示例如下;

  1. >>> import example
  2. >>> p = example.Pet("Molly")
  3. >>> print(p)
  4. <example.Pet object at 0x10cd98060>
  5. >>> p.getName()
  6. u'Molly'
  7. >>> p.setName("Charly")
  8. >>> p.getName()
  9. u'Charly'

See also:静态成员函数需要使用class_::def_static来绑定。

5.2 关键字参数和默认参数

可以使用第4章讨论的语法来指定关键字和默认参数,详见第4章相关章节。

5.3 绑定匿名函数

使用print(p)打印对象信息时,上面的例子会得到一些基本无用的信息。

  1. >>> print(p)
  2. <example.Pet object at 0x10cd98060>

我们可以绑定一个工具函数到__repr__方法,来返回可读性好的摘要信息。在不改变Pet类的基础上,使用一个匿名函数来完成这个功能是一个不错的选择。

  1. py::class_<Pet>(m, "Pet")
  2. .def(py::init<const std::string &>())
  3. .def("setName", &Pet::setName)
  4. .def("getName", &Pet::getName)
  5. .def("__repr__",
  6. [](const Pet &a) {
  7. return "<example.Pet named '" + a.name + "'>";
  8. });

通过上面的修改,Python中的输出如下:

  1. >>> print(p)
  2. <example.Pet named 'Molly'>

pybind11支持无状态和有状态的lambda闭包,即lambda表达式的[]是否带捕获参数。

5.4 成员变量

使用class_::def_readwrite方法可以导出公有成员变量,使用class_::def_readonly方法则可以导出只读成员。

  1. py::class_<Pet>(m, "Pet")
  2. .def(py::init<const std::string &>())
  3. .def_readwrite("name", &Pet::name)
  4. // ... remainder ...

Python中使用示例如下:

  1. >>> p = example.Pet("Molly")
  2. >>> p.name
  3. u'Molly'
  4. >>> p.name = "Charly"
  5. >>> p.name
  6. u'Charly'

假设Pet::name是一个私有成员变量,向外提供setter和getters方法。

  1. class Pet {
  2. public:
  3. Pet(const std::string &name) : name(name) { }
  4. void setName(const std::string &name_) { name = name_; }
  5. const std::string &getName() const { return name; }
  6. private:
  7. std::string name;
  8. };

可以使用class_::def_property()(只读成员使用class_::def_property_readonly())来定义并私有成员,并生成相应的setter和geter方法:

  1. py::class_<Pet>(m, "Pet")
  2. .def(py::init<const std::string &>())
  3. .def_property("name", &Pet::getName, &Pet::setName)
  4. // ... remainder ...

只写属性通过将read函数定义为nullptr来实现。

see also: 相似的方法class_::def_readwrite_static(), class_::def_readonly_static() class_::def_property_static(), class_::def_property_readonly_static()用于绑定静态变量和属性。

5.5 动态属性

原生的Pyhton类可以动态地获取新属性:

  1. >>> class Pet:
  2. ... name = "Molly"
  3. ...
  4. >>> p = Pet()
  5. >>> p.name = "Charly" # overwrite existing
  6. >>> p.age = 2 # dynamically add a new attribute

默认情况下,从C++导出的类不支持动态属性,其可写属性必须是通过class_::def_readwriteclass_::def_property定义的。试图设置其他属性将产生错误:

  1. >>> p = example.Pet()
  2. >>> p.name = "Charly" # OK, attribute defined in C++
  3. >>> p.age = 2 # fail
  4. AttributeError: 'Pet' object has no attribute 'age'

要让C++类也支持动态属性,我们需要在py::class_的构造函数添加py::dynamic_attr标识:

  1. py::class_<Pet>(m, "Pet", py::dynamic_attr())
  2. .def(py::init<>())
  3. .def_readwrite("name", &Pet::name);

这样,之前报错的代码就能够正常运行了。

  1. >>> p = example.Pet()
  2. >>> p.name = "Charly" # OK, overwrite value in C++
  3. >>> p.age = 2 # OK, dynamically add a new attribute
  4. >>> p.__dict__ # just like a native Python class
  5. {'age': 2}

需要提醒一下,支持动态属性会带来小小的运行时开销。不仅仅因为增加了额外的__dict__属性,还因为处理循环引用时需要花费更多的垃圾收集跟踪花销。但是不必担心这个问题,因为原生Python类也有同样的开销。默认情况下,pybind11导出的类比原生Python类效率更高,使能动态属性也只是让它们处于同等水平而已。

5.6 继承与向下转型

现在有两个具有继承关系的类:

  1. struct Pet {
  2. Pet(const std::string &name) : name(name) { }
  3. std::string name;
  4. };
  5. struct Dog : Pet {
  6. Dog(const std::string &name) : Pet(name) { }
  7. std::string bark() const { return "woof!"; }
  8. };

pybind11提供了两种方法来指明继承关系:1)将C++基类作为派生类class_的模板参数;2)将基类名作为class_的参数绑定到派生类。两种方法是等效的。

  1. py::class_<Pet>(m, "Pet")
  2. .def(py::init<const std::string &>())
  3. .def_readwrite("name", &Pet::name);
  4. // Method 1: template parameter:
  5. py::class_<Dog, Pet /* <- specify C++ parent type */>(m, "Dog")
  6. .def(py::init<const std::string &>())
  7. .def("bark", &Dog::bark);
  8. // Method 2: pass parent class_ object:
  9. py::class_<Dog>(m, "Dog", pet /* <- specify Python parent type */)
  10. .def(py::init<const std::string &>())
  11. .def("bark", &Dog::bark);

指明继承关系后,派生类实例将获得两者的字段和方法:

  1. >>> p = example.Dog("Molly")
  2. >>> p.name
  3. u'Molly'
  4. >>> p.bark()
  5. u'woof!'

上面的例子是一个常规非多态的继承关系,表现在Python就是:

  1. // 返回一个指向派生类的基类指针
  2. m.def("pet_store", []() { return std::unique_ptr<Pet>(new Dog("Molly")); });
  1. >>> p = example.pet_store()
  2. >>> type(p) # `Dog` instance behind `Pet` pointer
  3. Pet # no pointer downcasting for regular non-polymorphic types
  4. >>> p.bark()
  5. AttributeError: 'Pet' object has no attribute 'bark'

pet_store函数返回了一个Dog实例,但由于基类并非多态类型,Python只识别到了Pet。在C++中,一个类至少有一个虚函数才会被视为多态类型。pybind11会自动识别这种多态机制。

  1. struct PolymorphicPet {
  2. virtual ~PolymorphicPet() = default;
  3. };
  4. struct PolymorphicDog : PolymorphicPet {
  5. std::string bark() const { return "woof!"; }
  6. };
  7. // Same binding code
  8. py::class_<PolymorphicPet>(m, "PolymorphicPet");
  9. py::class_<PolymorphicDog, PolymorphicPet>(m, "PolymorphicDog")
  10. .def(py::init<>())
  11. .def("bark", &PolymorphicDog::bark);
  12. // Again, return a base pointer to a derived instance
  13. m.def("pet_store2", []() { return std::unique_ptr<PolymorphicPet>(new PolymorphicDog); });
  1. >>> p = example.pet_store2()
  2. >>> type(p)
  3. PolymorphicDog # automatically downcast
  4. >>> p.bark()
  5. u'woof!'

pybind11会自动地将一个指向多态基类的指针,向下转型为实际的派生类类型。这和C++常见的情况不同,我们不仅可以访问基类的虚函数,还能获取到通过基类看不到的,具体的派生类的方法和属性。

5.7 重载方法

重载方法即拥有相同的函数名,但入参不一样的函数:

  1. struct Pet {
  2. Pet(const std::string &name, int age) : name(name), age(age) { }
  3. void set(int age_) { age = age_; }
  4. void set(const std::string &name_) { name = name_; }
  5. std::string name;
  6. int age;
  7. };

我们在绑定Pet::set时会报错,因为编译器并不知道用户想选择哪个重载方法。我们需要添加具体的函数指针来消除歧义。绑定多个函数到同一个Python名称,将会自动创建函数重载链。Python将会依次匹配,找到最合适的重载函数。

  1. py::class_<Pet>(m, "Pet")
  2. .def(py::init<const std::string &, int>())
  3. .def("set", static_cast<void (Pet::*)(int)>(&Pet::set), "Set the pet's age")
  4. .def("set", static_cast<void (Pet::*)(const std::string &)>(&Pet::set), "Set the pet's name");

在函数的文档描述中,我们可以看见重载的函数签名:

  1. >>> help(example.Pet)
  2. class Pet(__builtin__.object)
  3. | Methods defined here:
  4. |
  5. | __init__(...)
  6. | Signature : (Pet, str, int) -> NoneType
  7. |
  8. | set(...)
  9. | 1. Signature : (Pet, int) -> NoneType
  10. |
  11. | Set the pet's age
  12. |
  13. | 2. Signature : (Pet, str) -> NoneType
  14. |
  15. | Set the pet's name

如果你的编译器支持C++14,也可以使用下面的语法来转换重载函数:

  1. py::class_<Pet>(m, "Pet")
  2. .def("set", py::overload_cast<int>(&Pet::set), "Set the pet's age")
  3. .def("set", py::overload_cast<const std::string &>(&Pet::set), "Set the pet's name");

这里,py::overload_cast仅需指定函数类型,不用给出返回值类型,以避免原语法带来的不必要的干扰(void (Pet::*))。如果是基于const的重载,需要使用py::const标识。

  1. struct Widget {
  2. int foo(int x, float y);
  3. int foo(int x, float y) const;
  4. };
  5. py::class_<Widget>(m, "Widget")
  6. .def("foo_mutable", py::overload_cast<int, float>(&Widget::foo))
  7. .def("foo_const", py::overload_cast<int, float>(&Widget::foo, py::const_));

如果你想在仅支持c++11的编译器上使用py::overload_cast语法,可以使用py::detail::overload_cast_impl来代替:

  1. template <typename... Args>
  2. using overload_cast_ = pybind11::detail::overload_cast_impl<Args...>;
  3. py::class_<Pet>(m, "Pet")
  4. .def("set", overload_cast_<int>()(&Pet::set), "Set the pet's age")
  5. .def("set", overload_cast_<const std::string &>()(&Pet::set), "Set the pet's name");

Note: 如果想定义多个重载的构造函数,使用.def(py::init<...>())语法依次定义就好,指定关键字和默认参数的机制也还是生效的。

5.8 枚举和内部类型

现在有一个含有枚举和内部类型的类:

  1. struct Pet {
  2. enum Kind {
  3. Dog = 0,
  4. Cat
  5. };
  6. struct Attributes {
  7. float age = 0;
  8. };
  9. Pet(const std::string &name, Kind type) : name(name), type(type) { }
  10. std::string name;
  11. Kind type;
  12. Attributes attr;
  13. };

绑定代码如下所示:

  1. py::class_<Pet> pet(m, "Pet");
  2. pet.def(py::init<const std::string &, Pet::Kind>())
  3. .def_readwrite("name", &Pet::name)
  4. .def_readwrite("type", &Pet::type)
  5. .def_readwrite("attr", &Pet::attr);
  6. py::enum_<Pet::Kind>(pet, "Kind")
  7. .value("Dog", Pet::Kind::Dog)
  8. .value("Cat", Pet::Kind::Cat)
  9. .export_values();
  10. py::class_<Pet::Attributes> attributes(pet, "Attributes")
  11. .def(py::init<>())
  12. .def_readwrite("age", &Pet::Attributes::age);

为确保嵌套类型KindAttributesPet的作用域中创建,我们必须向enum_class_的构造函数提供Pet class_实例。enum_::export_values()用来导出枚举项到父作用域,C++11的强枚举类型需要跳过这点。

  1. >>> p = Pet("Lucy", Pet.Cat)
  2. >>> p.type
  3. Kind.Cat
  4. >>> int(p.type)
  5. 1L

枚举类型的枚举项会被导出到类__members__属性中:

  1. >>> Pet.Kind.__members__
  2. {'Dog': Kind.Dog, 'Cat': Kind.Cat}

name属性可以返回枚举值的名称的unicode字符串,str(enum)也可以做到,但两者的实现目标不同。下面的例子展示了两者的差异:

  1. >>> p = Pet("Lucy", Pet.Cat)
  2. >>> pet_type = p.type
  3. >>> pet_type
  4. Pet.Cat
  5. >>> str(pet_type)
  6. 'Pet.Cat'
  7. >>> pet_type.name
  8. 'Cat'

Note: 当我们给enum_的构造函数增加py::arithmetic()标识时,pybind11将创建一个支持基本算术运算和位运算(如比较、或、异或、取反等)的枚举类型。

  1. py::enum_<Pet::Kind>(pet, "Kind", py::arithmetic())
  2. ...

默认情况下,省略这些可以节省内存空间。