说明
我们都知道 C++ 是支持函数重载和默认参数的,而 Python 语法本身虽然不支持重载,但是因其动态语言的灵活性,也可以视为有支持重载的特点。那么在使用 Boost.Python 将 C++ 程序封装成 Python 接口时,该如何处理默认参数和函数重载呢?
Boost.Python 提供了两种方法来实现重载函数的封装:手动封装和自动封装。后者简单而且可以传递默认参数,但能应用的函数有局限。前者能处理各种重载函数,但操作上麻烦一点点且不能传递默认参数。两者对比如下:
优点 | 缺点 | |
---|---|---|
手动封装 | 能应用于各种重载函数 | 会丢失默认参数,操作复杂 |
自动封装 | 可以保留默认参数,操作简单 | 重载函数时只能应用于有固定参数序列和相同返回值类型的函数 |
上面两种方法没有绝对的好坏之分,在开发过程中,我们往往需要根据实际情况两者结合起来使用。
封装重载函数
手动封装
以下面的 C++ 类为例,实现手动重载函数的操作。
class Test1
{
public:
bool func(int a) { return true; }
bool func(int a, int b) { return true; }
bool func (int a, float b, char c) { return true; }
int func(int a, int b, int c) {return a + b + c; }
};
该类包含4个重载成员函数 func()。
首先声明这些成员函数指针变量。
bool (Test1::*func1)(int) = &Test1::func;
bool (Test1::*func2)(int, int) = &Test1::func;
bool (Test1::*func3)(int, float, char) = &Test1::func;
int (Test1::*func4)(int, int, int) = &Test1::func;
然后在 Python 模块中封装这些函数指针:
namespace bp = boost::python;
BOOST_PYTHON_MODULE(overload)
{
bp::class_<Test1> ("Test1")
.def ("func", func1, bp::args("a"))
.def ("func", func2, bp::args("a", "b"))
.def ("func", func3, bp::args("a", "b", "c"))
.def ("func", func4, bp::args("a", "b"))
;
}
编译后运行:
$python
>>> import overload
>>> t1 = overload.Test1()
>>> t1.func(1)
True
>>> t1.func(1, 2)
True
>>> t1.func(1, 0.5, 'c')
True
>>> t1.func(1, 2, 3)
6
可见,采用这种方法是可以实现重载函数的封装的,但是一方面比较麻烦,需要将每种重载函数声明一个独立的指针。另一方面,如果这些函数含有默认参数是无法传递给 Python 的方法的。
自动封装
所谓自动封装是指我们不再需要像手动封装那样指定 Python 接口调用哪一个 C++ 函数,这一个过程由 Boost.Python 帮我们完成。
先换一个例子来看,定义一个重载函数 foo():
void foo ()
{
std::cout << "The first foo has no args." << std::endl;
}
void foo (int a)
{
std::cout << "The sencond foo has one arg: " << a << std::endl;
}
void foo (int a, float b)
{
std::cout << "The third foo has two args: " << a << " " << b << std::endl;
}
void foo (int a, float b, char c)
{
std::cout << "The forth foo has three args: " << a << " " << b << " " << c << std::endl;
}
foo() 函数有四个实现,分别包含 0-3 个参数。
在 Python 模块中封装:
using namespace boost::python;
BOOST_PYTHON_FUNCTION_OVERLOADS(foo_overloads, foo, 0, 3)
BOOST_PYTHON_MODULE(overload)
{
def("foo", (void (*)(int, float, char))0, foo_overloads())
;
}
这里使用 BOOST_PYTHON_FUNCTION_OVERLOADS
宏来生成一个重载函数生成器 foo_overloads
,其对应的函数是 foo(),后面的0和3分别表示函数支持的最少参数和最多参数个数。
def()
函数将自动为我们添加所有 foo 变体。原本 C++ 函数的位置使用函数指针类型替代,后接重载函数生成器 foo_overloads
。
编译后运行:
$ python
>>> import overload as ol
>>> ol.foo()
The first foo has no args.
>>> ol.foo(1)
The sencond foo has one arg: 1
>>> ol.foo(2, 0.5)
The third foo has two args: 2 0.5
>>> ol.foo(3, 0.4, 'c')
The forth foo has three args: 3 0.4 c
>>>
自动封装的局限性
在上面的例子中,我们使用自动封装宏函数实现了重载函数的自动封装,但是仔细观察上面的示例,发现 foo()
所有的重载函数的返回类型都是 void,且虽然支持 0 - 3 个不同数量的参数,但这些参数都有固定的顺序。即后一个函数参数是前一个函数参数序列的扩展。
假如我们修改其中一个函数的返回值或参数类型,如:
int foo(int a);
// 或者
void foo (int a, int b,int c)
编译会报错:
error: no matching function for call to ‘foo(foo_overloads::non_void_return_type::gen<boost::mpl::vector4<void, int, float, char> >::T0&, foo_overloads::non_void_return_type::gen<boost::mpl::vector4<void, int, float, char> >::T1&)’
86 | BOOST_PYTHON_FUNCTION_OVERLOADS(foo_overloads, foo, 0, 3)
...
error: return-statement with a value, in function returning ‘foo_overloads::non_void_return_type::gen<boost::mpl::vector4<void, int, float, char> >::RT’ {aka ‘void’} [-fpermissive]
86 | BOOST_PYTHON_FUNCTION_OVERLOADS(foo_overloads, foo, 0, 3)
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
make[2]: *** [CMakeFiles/overload.dir/build.make:63: CMakeFiles/overload.dir/overload.cpp.o] Error 1
make[1]: *** [CMakeFiles/Makefile2:76: CMakeFiles/overload.dir/all] Error 2
make: *** [Makefile:84: all] Error 2
那如果在原有的4个函数基础上增加一个函数呢,比如:
void foo (int a, char b)
编译时发现同样会报错。
由此可见,自动封装重载函数需要目标函数的返回值类型一致,且遵循确定的参数序列。
传递默认参数
手动传递
在第一个示例中,使用 Boost.Python 手动封装函数指针不会携带 C++ 函数的默认参数信息。
比如取一个有默认参数参数的函数 f:
int f(int, double = 3.14, char const* = "hello");
指向函数 f 的指针的类型没有关于其默认参数的信息:
int(*g)(int,double,char const*) = f; // 没有默认参数
然后封装函数指针 g 时无法传递默认参数:
def("g", g); // 默认参数丢失
因此,如果要想默认参数信息传递给 Python 接口,我们需要在原有的 C++ 函数基础之上再套一层函数:
// write "thin wrappers"
int f1(int x) { return f(x); }
int f2(int x, double y) { return f(x,y); }
/*...*/
// in module init
def("f", f); // all arguments
def("f", f2); // two arguments
def("f", f1); // one argument
这里额外定义了两个函数 f1 和 f2,其中 f1 保留了 f 中的两个默认参数, f2 保留了最后一个默认参数。
自动传递
与前面的自动封装重载函数一样,自动传递默认参数时我们也要借助 BOOST_PYTHON_FUNCTION_OVERLOADS
宏函数。例如,给定一个函数:
int foo(int a, char b = 1, unsigned c = 2, double d = 3)
{
/*...*/
}
使用宏调用:
BOOST_PYTHON_FUNCTION_OVERLOADS(foo_overloads, foo, 1, 4)
在 Python 模块中命令与之前一样:
def("foo", foo, foo_overloads());
初始化和可选参数
对类构造函数也可以使用这个工具来传递默认参数或一系列重载函数。还记得 init<…>
吗?例如,给定一个带有构造函数的类 X:
struct X
{
X(int a, char b = 'D', std::string c = "constructor", double d = 0.0);
/*...*/
}
我们可以轻松地将此构造函数一次性添加到 Boost.Python:
.def(init<int, optional<char, std::string, double> >())
// 或者
class_<X> ("X", init<int, optional<char, std::string, double> >())
宏函数
前面的内容结合示例基本介绍了重载函数和默认参数的处理,这里我们对前面用到的宏函数作用和定义做个说明。
宏使用格式
前面的示例中我们只用到了 BOOST_PYTHON_FUNCTION_OVERLOADS
宏,该宏只能使用于普通函数或者静态函数,如果目标是类的成员函数,则需要使用它的姊妹:BOOST_PYTHON_MEMBER_FUNCTION_OVERLOADS
。这两个宏的使用格式如下:
// For global functions and static methods:
BOOST_PYTHON_FUNCTION_OVERLOADS( overloadsname , functionname , arg_minimum, arg_maximum )
BOOST_PYTHON_FUNCTION_OVERLOADS( overloadsname , classname::staticmethodname , arg_minimum, arg_maximum )
// For class methods:
BOOST_PYTHON_MEMBER_FUNCTION_OVERLOADS( overloadsname , classname::methodname, arg_minimum, arg_maximum )
格式和参数完全一致:
- overloadsname : 重载调度生成器
- functionname/staticmethodname/methodname : 要封装的函数
- arg_minimum : 最小参数个数
- arg_maximum : 最大参数个数
宏定义
宏定义在文件 boost/python/detail/defaults_gen.hpp 头文件中,但在实际编程时,我们只要包含 <boost/python/overloads.hpp> 头文件就好。
#define BOOST_PYTHON_FUNCTION_OVERLOADS(generator_name, fname, min_args, max_args) \
BOOST_PYTHON_GEN_FUNCTION_STUB( \
fname, \
generator_name, \
max_args, \
BOOST_PP_SUB_D(1, max_args, min_args))
#define BOOST_PYTHON_MEMBER_FUNCTION_OVERLOADS(generator_name, fname, min_args, max_args) \
BOOST_PYTHON_GEN_MEM_FUNCTION_STUB( \
fname, \
generator_name, \
max_args, \
BOOST_PP_SUB_D(1, max_args, min_args))
重载调度生成器
在前面的描述里经常提到的 overloadsname/generator_name
就是重载调度生成器,它会为扩展类生成的一系列重载方法。具有如下属性:
- docstring : 与 Python ‘_doc_’ 属性绑定的描述信息;
- keywords : 关键字表达式,用于明确生成方法的参数;
- call policies : 一个 CallPolices 实例;
- minimum arity : 重载函数接受的最小的参数个数;
- maximum arity : 重载函数接受的最大的参数个数;
调度生成器的定义使用 BOOST_PYTHON_
,而其使用可以包含上述属性(也可以不包含),如下所示:
overloadsname()
overloadsname(docstring)
overloadsname(docstring, keywords)
overloadsname(keywords, docstring)
overloadsname()[policies]
overloadsname(docstring)[policies]
overloadsname(docstring, keywords)[policies]
overloadsname(keywords, docstring)[policies]
示例
#include <boost/python/module.hpp>
#include <boost/python/def.hpp>
#include <boost/python/args.hpp>
#include <boost/python/tuple.hpp>
#include <boost/python/class.hpp>
#include <boost/python/overloads.hpp>
#include <boost/python/return_internal_reference.hpp>
using namespace boost::python;
tuple f(int x = 1, double y = 4.25, char const* z = "wow")
{
return make_tuple(x, y, z);
}
BOOST_PYTHON_FUNCTION_OVERLOADS(f_overloads, f, 0, 3)
struct Y {};
struct X
{
Y& f(int x, double y = 4.25, char const* z = "wow")
{
return inner;
}
Y inner;
};
BOOST_PYTHON_MEMBER_FUNCTION_OVERLOADS(f_member_overloads, f, 1, 3)
BOOST_PYTHON_MODULE(args_ext)
{
def("f", f,
f_overloads(
args("x", "y", "z"), "This is f's docstring"
));
class_<Y>("Y")
;
class_<X>("X", "This is X's docstring")
.def("f1", &X::f,
f_member_overloads(
args("x", "y", "z"), "f's docstring"
)[return_internal_reference<>()]
)
;
}
参考资料
boost.python FunctionOverloading
Overloading
boost/python/overloads.hpp