如何用pybind11为可变参数模板可调用对象生成Python绑定?
使用pybind11为C++可变参数函数创建Python绑定方案
问题背景
我正尝试使用pybind11为一个包含大量可变参数(成员)函数或构造函数的C17库创建Python绑定。示例C代码如下:
// 可变参数成员函数 struct Bar { template <typename... Args> void baz(Args&&...){ /*...*/ } }; // 可变参数构造函数 struct Foo { template <typename... Args> Foo(Args&&...){ /*...*/ } }; // 可变参数自由函数 template <typename... Args> void foobar(Args&&...){ /*...*/ }
我了解pybind11支持位置参数*args,但不清楚如何用pybind11::args将参数转发给这些可变参数函数。目前写的绑定框架代码如下:
#include <pybind11/pybind11.h> namespace py = pybind11; PYBIND11_MODULE(example, m) { py::class_<Bar>(m, "Bar") .def("baz", [](py::args){ /* 如何转发给可变参数Bar::baz? */ }); py::class_<Foo>(m, "Foo") .def(py::init<py::args>()); // 这样写是否正确? m.def("foobar", [](py::args){ /* 如何转发给可变参数foobar? */ }); }
约束条件
- 只需支持特定固定类型的异构参数,例如仅暴露接受任意数量
Bar实例的Foo构造函数即可; - 不愿修改现有库的API,可接受绑定代码中的胶水代码,但需具备可维护性;
- 可接受将可变参数数量限制为最大值(如20),需为Python用户提供参数超出上限的报错信息;
- 不限于pybind11,也可使用其他更合适的库。
解决方案
一、特定类型可变参数构造函数绑定(以Foo为例)
直接写py::init<py::args>()无法正确转发参数,因为pybind11无法自动推断模板参数。我们可以通过递归模板提取py::args中的参数,同时验证类型和数量上限。
以下是针对仅接受Bar实例的Foo构造函数的绑定代码:
#include <pybind11/pybind11.h> #include <stdexcept> #include <utility> namespace py = pybind11; // 递归模板:逐步提取参数并转发 template <size_t CurrentIdx, typename... ExtractedArgs> struct ForwardBarArgs { static Foo* construct(py::args args, ExtractedArgs&&... extracted) { // 提取当前索引的参数并转换为Bar引用 auto& bar = args[CurrentIdx].cast<Bar&>(); return ForwardBarArgs<CurrentIdx + 1, ExtractedArgs..., Bar&>::construct( args, std::forward<ExtractedArgs>(extracted)..., bar ); } }; // 终止模板:达到参数上限时检查剩余参数 template <typename... ExtractedArgs> struct ForwardBarArgs<20, ExtractedArgs...> { static Foo* construct(py::args args, ExtractedArgs&&... extracted) { if (args.size() > 20) { throw std::runtime_error("参数数量超出上限,最多支持20个Bar实例"); } return new Foo(std::forward<ExtractedArgs>(extracted)...); } }; PYBIND11_MODULE(example, m) { // 先绑定Bar类(假设Bar有默认构造函数) py::class_<Bar>(m, "Bar") .def(py::init<>()); // 绑定Foo的构造函数 py::class_<Foo>(m, "Foo") .def(py::init([](py::args args) { // 验证所有参数都是Bar类型 for (const auto& arg : args) { if (!py::isinstance<Bar>(arg)) { throw std::runtime_error("所有参数必须是Bar实例"); } } if (args.empty()) { return new Foo(); // 处理无参数情况 } return ForwardBarArgs<0>::construct(args); })); }
二、可变参数成员函数绑定(Bar::baz)
如果baz仅支持特定类型(比如int和std::string),可以用switch-case处理有限数量的参数版本,代码更直观:
py::class_<Bar>(m, "Bar") .def(py::init<>()) .def("baz", [](Bar& self, py::args args) { const size_t arg_count = args.size(); if (arg_count > 20) { throw std::runtime_error("最多支持20个参数"); } // 验证参数类型(示例:仅允许int或str) for (const auto& arg : args) { if (!py::isinstance<int>(arg) && !py::isinstance<std::string>(arg)) { throw std::runtime_error("参数必须是int或str类型"); } } // 根据参数数量转发 switch(arg_count) { case 0: self.baz(); break; case 1: self.baz(args[0].cast<int>()); break; case 2: self.baz(args[0].cast<int>(), args[1].cast<std::string>()); break; // ... 依次添加到20个参数的情况 default: throw std::runtime_error("参数数量超出上限"); } });
三、可变参数自由函数绑定(foobar)
和成员函数逻辑一致,以仅支持float参数为例:
m.def("foobar", [](py::args args) { const size_t arg_count = args.size(); if (arg_count > 20) { throw std::runtime_error("最多支持20个参数"); } // 验证参数类型 for (const auto& arg : args) { if (!py::isinstance<float>(arg)) { throw std::runtime_error("参数必须是float类型"); } } // 转发参数 switch(arg_count) { case 0: foobar(); break; case 1: foobar(args[0].cast<float>()); break; case 2: foobar(args[0].cast<float>(), args[1].cast<float>()); break; // ... 依次添加到20个参数的情况 default: throw std::runtime_error("参数数量超出上限"); } });
四、替代方案:使用Boost.Python
如果pybind11的模板写法过于繁琐,Boost.Python可以通过直接绑定多版本函数的方式处理:
#include <boost/python.hpp> #include <utility> namespace bp = boost::python; // 包装函数:转发参数给原foobar template <typename... Args> void wrap_foobar(Args&&... args) { foobar(std::forward<Args>(args)...); } BOOST_PYTHON_MODULE(example) { // 绑定Bar类 bp::class_<Bar>("Bar") .def("baz", bp::raw_function([](bp::object self, bp::args args) { // 处理逻辑同pybind11的成员函数绑定 return bp::object(); })); // 绑定Foo构造函数 bp::class_<Foo>("Foo", bp::init<bp::args>()); // 绑定多个版本的foobar(从0到20个参数) bp::def("foobar", &wrap_foobar<>); bp::def("foobar", &wrap_foobar<float>); bp::def("foobar", &wrap_foobar<float, float>); // ... 依次添加到20个float参数的版本 }
内容的提问来源于stack exchange,提问作者joergbrech
相关产品推荐
相关产品推荐

