PyBind11底层其实就是CPython的各个API调用,但使用C++做了良好的封装。
PyBind11中大量应用了现代C++技巧,如变长参数模板(variadic template)、lambda表达式,同时也使用了一些传统的C++技巧和设计模式,如奇异递归模板模式(CRTP,Curiously Recurring Template Pattern)。
下面的所有说明遵循官方的惯例,使用如下头文件和命名空间别名:
#include namespace py = pybind11;
模块入口函数
导入模块:
PYBIND11_MODULE(test, m) {
m.doc() = "PyBind11 Test";
m.def("add", [](int i, int j) { return i + j; });
}
该宏PYBIND11_MODULE定义了函数PYBIND11_PLUGIN_IMPL(name):
#define PYBIND11_MODULE(name, variable) \static void PYBIND11_CONCAT(pybind11_init_, name)(pybind11::module&); \PYBIND11_PLUGIN_IMPL(name) { \PYBIND11_CHECK_PYTHON_VERSION \auto m = pybind11::module(pybind11_init_, name)(m); \try { \PYBIND11_CONCAT(pybind11_init_, name)(m); \return m.ptr(); \} PYBIND11_CATCH_INIT_EXCEPTIONS \} \void PYBIND11_CONCAT(pybind11_init_, name)(pybind11::module& variable)
这个函数名会宏展开为extern "C" PyObject* PyInit_##name()
#define PYBIND11_PLUGIN_IMPL(name) \extern "C" PYBIND11_EXPORT PyObject* PyInit_##name(); \extern "C" PYBIND11_EXPORT PyObject* PyInit_##name()
而CPython需要导入的模块实现的即这个函数。
C++函数的封装
C++函数被封装在类cpp_function中,它允许接受的C++“函数”有:普通函数指针(形如R (*)(Args ...))
函数对象(亦称仿函数)
lambda表达式实际上就是仿函数的语法糖
具有R operator()(Args ...)的括号运算符重载)
类的成员函数指针(形如R (C::*)(Args ...))
类的常成员函数指针(形如R (C::*)(Args ...) const)。
函数类cpp_function
C++侧
概括来说,cpp_function使用了和std::function类似的类型擦除(type erasure)方法。在模板化的构造函数中趁着还有目标函数(被绑定的C++函数)的类型信息,完成如下变换:把类的成员函数通过lambda转化为仿函数:
Result (Class::*f)(Args ...) /* const */;
[f](/* const */ Class* c, Args... args) -> Result {
return c->f(args...);
};此后都可以按照普通函数来调用。实现一个普通函数impl将std::vector中存储的参数转发给被绑定的C++函数
最终将上述函数impl转化为PyObject备用
CPython侧
PyBind11使用CPython的API函数PyCFunction_NewEx创建Python函数PyCFunctionObject;使用PyInstanceMethod_New(Python 3)或者PyMethod_New(Python 2)来创建一个PyObject。
辅助函数def
类module的成员函数def就接受一个函数名,一个C++“函数”,以及数个额外属性:
template
module& module::def(const char* name_, Func&& f, const Extra& ... extra);
其中函数f会被完美转发给cpp_function的构造函数。返回值为module&用于支持连续的def调用。
额外属性可以是类型为arg的对象,用于表示Python的具名参数。PyBind11定义了用户自定义字面量constexpr arg operator"" _a(const char* name, size_t length);这允许我们直接将调用py::arg("i")写为字面量"i"_a。此外,arg重载了赋值运算符,可以通过"i"_a = 1表示一个具有默认形参1的具名参数i。
最终,函数f通过CPython的PyModule_AddObject函数注册到Python侧,但并不是作为Python函数,而是作为一个可调用对象(实现了__call__方法的对象)。
Python属性
函数attr接受一个const char*(空字符结尾的C字符串)或者一个句柄handle(持有另一个Python对象py::object),这个参数作为Python中访问它所使用的键(key)。attr返回一个可赋值的obj_attr_accessor或str_attr_accessor对象,对其赋值则会指定该属性名对应的Python对象。
// 直接赋值m.attr("the_answer") = 42;
// 或者,使用 py::cast 转换m.attr("what") = py::cast("World");
这将最终在PyBind11内部转化为对CPython的API函数PyObject_SetAttr和PyObject_SetAttrString的调用。
C++类的封装
首先创建一个py::class_类型的变量,然后使用def和attr向类中添加属性和方法。py::class_最终继承自py::object,因此attr函数的描述可以参考上面的说明。成员函数def有新的定义,并且没有显式using父类的def,因此父类的def被隐藏。
这里只讨论最常用的def版本。它的函数签名仍然和上面描述的一致,接受一个函数名,一个C++”函数“,以及一组额外属性。不同的是,这里的处理方法是将这个cpp_function作为该类的一个属性(对于模块是直接添加为模块的对象),不过相同的是在Python一侧看来它都是一个可调用对象。
鸣谢:本文及其余博文使用Jekyll生成。