7. 函数

在开始本节前,请确保你已经熟悉了第4章和第5章讲述的函数和类绑定的基本方法。下面我们将继续讲述普通函数、成员函数、以及Python方法的知识点。

7.1 返回值策略

Python和C++在管理内存和对象生命周期管理上存在本质的区别。这导致我们在创建返回no-trivial类型的函数绑定时会出问题。仅通过类型信息,我们无法明确是Python侧需要接管返回值并负责释放资源,还是应该由C++侧来处理。因此,pybind11提供了一些返回值策略来确定由哪方管理资源。这些策略通过model::def()class_def()来指定,默认策略为return_value_policy::automatic

返回值策略难以捉摸,正确地选择它们则显得尤为重要。下面我们通过一个简单的例子来阐释选择错误的情形:

  1. /* Function declaration */
  2. Data *get_data() { return _data; /* (pointer to a static data structure) */ }
  3. ...
  4. /* Binding code */
  5. m.def("get_data", &get_data); // <-- KABOOM, will cause crash when called from Python

当Python侧调用get_data()方法时,返回值(原生C++类型)必须被转换为合适的Python类型。在这个例子中,默认的返回值策略(return_value_policy::automatic)使得pybind11获取到了静态变量_data的所有权。

当Python垃圾收集器最终删除_data的Python封装时,pybind11将尝试删除C++实例(通过operator delete())。这时,这个程序将以某种隐蔽的错误并涉及静默数据破坏的方式崩溃。

对于上面的例子,我们应该指定返回值策略为return_value_policy::reference,这样全局变量的实例仅仅被引用,而不涉及到所有权的转移:

  1. m.def("get_data", &get_data, py::return_value_policy::reference);

另一方面,引用策略在多数其他场合并不是正确的策略,忽略所有权的归属可能导致资源泄漏。作为一个使用pybind11的开发者,熟悉不同的返回值策略及其适用场合尤为重要。下面的表格将提供所有策略的概览:

返回值策略 描述
return_value_policy::take_ownership 引用现有对象(不创建一个新对象),并获取所有权。在引用计数为0时,Pyhton将调用析构函数和delete操作销毁对象。
return_value_policy::copy 拷贝返回值,这样Python将拥有拷贝的对象。该策略相对来说比较安全,因为两个实例的生命周期是分离的。
return_value_policy::move 使用std::move来移动返回值的内容到新实例,新实例的所有权在Python。该策略相对来说比较安全,因为两个实例的生命周期是分离的。
return_value_policy::reference 引用现有对象,但不拥有所有权。C++侧负责该对象的生命周期管理,并在对象不再被使用时负责析构它。注意:当Python侧还在使用引用的对象时,C++侧删除对象将导致未定义行为。
return_value_policy::reference_internal 返回值的生命周期与父对象的生命周期相绑定,即被调用函数或属性的thisself对象。这种策略与reference策略类似,但附加了keep_alive<0, 1>调用策略保证返回值还被Python引用时,其父对象就不会被垃圾回收掉。这是由def_propertydef_readwrite创建的属性getter方法的默认返回值策略。
return_value_policy::automatic 当返回值是指针时,该策略使用return_value_policy::take_ownership。反之对左值和右值引用使用return_value_policy::copy。请参阅上面的描述,了解所有这些不同的策略的作用。这是py::class_封装类型的默认策略。
return_value_policy::automatic_reference 和上面一样,但是当返回值是指针时,使用return_value_policy::reference策略。这是在C++代码手动调用Python函数和使用pybind11/stl.h中的casters时的默认转换策略。你可能不需要显式地使用该策略。

返回值策略也可以应用于属性:

  1. class_<MyClass>(m, "MyClass")
  2. .def_property("data", &MyClass::getData, &MyClass::setData,
  3. py::return_value_policy::copy);

在技术层面,上述代码会将策略同时应用于getter和setter函数,但是setter函数并不关心返回值策略,这样做仅仅出于语法简洁的考虑。或者,你可以通过cpp_function构造函数来传递目标参数:

  1. class_<MyClass>(m, "MyClass")
  2. .def_property("data"
  3. py::cpp_function(&MyClass::getData, py::return_value_policy::copy),
  4. py::cpp_function(&MyClass::setData)
  5. );

注意:代码使用无效的返回值策略将导致未初始化内存或多次free数据结构,这将导致难以调试的、不确定的问题和段错误。因此,花点时间来理解上面表格的各个选项是值得的。

提示

  1. 上述策略的另一个重点是,他们仅可以应用于pybind11还不知晓的实例,这时策略将澄清返回值的生命周期和所有权问题。当pybind11已经知晓参数(通过其在内存中的类型和地址来识别),它将返回已存在的Python对象封装,而不是创建一份拷贝。
  2. 下一节将讨论上面表格之外的调用策略,他涉及到返回值和函数参数的引用关系。
  3. 可以考虑使用智能指针来代替复杂的调用策略和生命周期管理逻辑。智能指针会告诉你一个对象是否仍被C++或Python引用,这样就可以消除各种可能引发crash或未定义行为的矛盾。对于返回智能指针的函数,没必要指定返回值策略。

7.2 附加的调用策略

除了以上的返回值策略外,进一步指定调用策略可以表明参数间的依赖关系,确保函数调用的稳定性。

保活(keep alive)

当一个C++容器对象包含另一个C++对象时,我们需要使用该策略。keep_alive<Nurse, Patient>表明至少在索引Nurse被回收前,索引Patient应该被保活。0表示返回值,1及以上表示参数索引。1表示隐含的参数this指针,而常规参数索引从2开始。当Nurse的值在运行前被检测到为None时,调用策略将什么都不做。

当nurse不是一个pybind11注册类型时,实现依赖于创建对nurse对象弱引用的能力。如果nurse对象不是pybind11注册类型,也不支持弱引用,程序将会抛出异常。

如果你使用一个错误的参数索引,程序将会抛出”Could not cativate keep_alive!”警告的运行时异常。这时,你应该review你代码中使用的索引。

参见下面的例子:一个list append操作,将新添加元素的生命周期绑定到添加的容器对象上:

  1. py::class_<List>(m, "List").def("append", &List::append, py::keep_alive<1, 2>());

为了一致性,构造函数的实参索引也是相同的。索引1仍表示this指针,索引0表示返回值(构造函数的返回值被认为是void)。下面的示例将构造函数入参的生命周期绑定到被构造对象上。

  1. py::class_<Nurse>(m, "Nurse").def(py::init<Patient &>(), py::keep_alive<1, 2>());

Note: keep_alive与Boost.Python中的with_custodian_and_wardwith_custodian_and_ward_postcall相似。

Call guard

call_guard<T>策略允许任意T类型的scope guard应用于整个函数调用。示例如下:

  1. m.def("foo", foo, py::call_guard<T>());

上面的代码等价于:

  1. m.def("foo", [](args...) {
  2. T scope_guard;
  3. return foo(args...); // forwarded arguments
  4. });

仅要求模板参数T是可构造的,如gil_scoped_release就是一个非常有用的类型。

call_guard支持同时制定多个模板参数,call_guard<T1, T2, T3 ...>。构造顺序是从左至右,析构顺序则相反。

See also: test/test_call_policies.cpp含有更丰富的示例来展示keep_alivecall_guard的用法。

7.3 以Python对象作为参数

pybind11通过简单的C++封装类,公开了绝大多数Python类型。这些封装类也可以在绑定代码宏作为函数参数使用,这样我们就可以在C++侧使用原生的python类型。举个遍历Python dict的例子:

  1. void print_dict(const py::dict& dict) {
  2. /* Easily interact with Python types */
  3. for (auto item : dict)
  4. std::cout << "key=" << std::string(py::str(item.first)) << ", "
  5. << "value=" << std::string(py::str(item.second)) << std::endl;
  6. }
  7. // it can be exported as follow:
  8. m.def("print_dict", &print_dict);

在Python中使用如下:

  1. >>> print_dict({"foo": 123, "bar": "hello"})
  2. key=foo, value=123
  3. key=bar, value=hello

7.4 接收*args**kwatgs参数

Python的函数可以接收任意数量的参数和关键字参数:

  1. def generic(*args, **kwargs):
  2. ... # do something with args and kwargs

我们也可以通过pybind11来创建这样的函数:

  1. void generic(py::args args, const py::kwargs& kwargs) {
  2. /// .. do something with args
  3. if (kwargs)
  4. /// .. do something with kwargs
  5. }
  6. /// Binding code
  7. m.def("generic", &generic);

py::args继承自py::tuplepy::kwargs继承自py::dict

更多示例参考test/test_kwargs_and_defualts.cpp

7.5 再探默认参数

前面的章节已经讨论了默认参数的基本用法。关于实现有一个值得关注的点,就是默认参数在声明时就被转换为Python对象了。看看下面的例子:

  1. py::class_<MyClass>("MyClass").def("myFunction", py::arg("arg") = SomeType(123));

这个例子里,必须保证SomeType类型已经被binding了(通过py::class_),不然就会抛出异常。

另一个值得注意的事情就是,生成的函数签名将使用对象的__repr__方法来处理默认参数值。如果对象没有提供该方法,那么函数签名将不能直观的看出默认参数值。

  1. FUNCTIONS
  2. | myFunction(...)
  3. | Signature : (MyClass, arg : SomeType = <SomeType object at 0x101b7b080>) -> NoneType

要处理这个问题,我们需要定义SomeType.__repr__方法,或者使用arg_v给默认参数手动添加方便阅读的注释。

  1. py::class_<MyClass>("MyClass")
  2. .def("myFunction", py::arg_v("arg", SomeType(123), "SomeType(123)"));
  3. ``
  4. 有时,可能需要使用空指针作为默认参数:
  5. ```c++
  6. py::class_<MyClass>("MyClass")
  7. .def("myFunction", py::arg("arg") = static_cast<SomeType *>(nullptr));

7.6 Keyword-only参数

Python3提供了keyword-only参数(在函数定义中使用*作为匿名参数):

  1. def f(a, *, b): # a can be positional or via keyword; b must be via keyword
  2. pass
  3. f(a=1, b=2) # good
  4. f(b=2, a=1) # good
  5. f(1, b=2) # good
  6. f(1, 2) # TypeError: f() takes 1 positional argument but 2 were given

pybind11提供了py::kw_only对象来实现相同的功能:

  1. m.def("f", [](int a, int b) { /* ... */ },
  2. py::arg("a"), py::kw_only(), py::arg("b"));

注意,该特性不能与py::args一起使用。

7.7 Positional-only参数

python3.8引入了Positional-only参数语法,pybind11通过py::pos_only()来提供相同的功能:

  1. m.def("f", [](int a, int b) { /* ... */ },
  2. py::arg("a"), py::pos_only(), py::arg("b"));

现在,你不能通过关键字来给定a参数。该特性可以和keyword-only参数一起使用。

7.8 Non-converting参数

有些参数可能支持类型转换,如:

  • 通过py::implicitly_convertible<A,B>()进行隐式转换
  • 将整形变量传给入参为浮点类型的函数
  • 将非复数类型(如float)传给入参为std::complex<float>类型的函数
  • Calling a function taking an Eigen matrix reference with a numpy array of the wrong type or of an incompatible data layout.

有时这种转换并不是我们期望的,我们可能更希望绑定代码抛出错误,而不是转换参数。通过py::arg来调用.noconvert()方法可以实现这个事情。

  1. m.def("floats_only", [](double f) { return 0.5 * f; }, py::arg("f").noconvert());
  2. m.def("floats_preferred", [](double f) { return 0.5 * f; }, py::arg("f"));

尝试进行转换时,将抛出TypeError异常:

  1. >>> floats_preferred(4)
  2. 2.0
  3. >>> floats_only(4)
  4. Traceback (most recent call last):
  5. File "<stdin>", line 1, in <module>
  6. TypeError: floats_only(): incompatible function arguments. The following argument types are supported:
  7. 1. (f: float) -> float
  8. Invoked with: 4

该方法可以与缩写符号_a和默认参数配合使用,像这样py::arg().noconvert()

7.9 允许/禁止空参数

当函数接受由py::class_注册的C++类型的指针或shared holder(如指针指针等),pybind11允许将Python的None传递给函数,等同于C++中传递nullptr给函数。

我们可以使用py::arg对象的.none方法来显式地使能或禁止该行为。

  1. py::class_<Dog>(m, "Dog").def(py::init<>());
  2. py::class_<Cat>(m, "Cat").def(py::init<>());
  3. m.def("bark", [](Dog *dog) -> std::string {
  4. if (dog) return "woof!"; /* Called with a Dog instance */
  5. else return "(no dog)"; /* Called with None, dog == nullptr */
  6. }, py::arg("dog").none(true));
  7. m.def("meow", [](Cat *cat) -> std::string {
  8. // Can't be called with None argument
  9. return "meow";
  10. }, py::arg("cat").none(false));

这样,Python调用bark(None)将返回"(no dog)",调用meow(None)将抛出异常TypeError

  1. >>> from animals import Dog, Cat, bark, meow
  2. >>> bark(Dog())
  3. 'woof!'
  4. >>> meow(Cat())
  5. 'meow'
  6. >>> bark(None)
  7. '(no dog)'
  8. >>> meow(None)
  9. Traceback (most recent call last):
  10. File "<stdin>", line 1, in <module>
  11. TypeError: meow(): incompatible function arguments. The following argument types are supported:
  12. 1. (cat: animals.Cat) -> str
  13. Invoked with: None

在不显式指定的情况下,默认支持传递None

Note: Even when .none(true) is specified for an argument, None will be converted to a nullptr only for custom and opaque types. Pointers to built-in types (double *, int *, …) and STL types (std::vector<T> *, …; if pybind11/stl.h is included) are copied when converted to C++ (see Overview) and will not allow None as argument. To pass optional argument of these copied types consider using std::optional<T>

7.10 重载解析顺序

当一个函数或者方法拥有多个重载时,pybind11通过两个步骤来决定重载调用的次序。第一步尝试不做类型匹配各个重载函数。如果没有匹配到,第二步将允许类型转换再匹配一次(显示调用py::arg().noconvert()禁用类型转换的函数除外)。

如果两步都失败了,将抛出异常TypeError

在上述两步中,重载函数将以pybind11中注册的顺序依次遍历。如果函数定义中增加了py::prepend()的标识,该重载函数将最先被遍历。

Note: pybind11不会根据重载参数的数量或类型来排优先级。换言之,pybind11不会将仅需一次类型转换的函数排在需要三次转换的函数前面,仅仅会将不需要类型转换的重载函数排在至少需要一次类型转换的函数前面。