python SWIG 对象比较
我有两个SWIG对象的列表:a和b。我需要进行集合或比较操作,以找出在a中但不在b中的项目。(我还有其他操作要做,但这个例子是个不错的起点。)
set(a) -set(b)
这样做的结果不准确,所以我尝试了:
[item for item in a if item not in b]
在这两种情况下,它都没有返回任何项目,尽管a和b之间没有共同的元素。
在a中我有一个值为:
<Swig Object of type 'OpenAccess_4::oaRect *' at 0x1ad6eea0>
而在b中有一个项目:
<Swig Object of type 'OpenAccess_4::oaRect *' at 0x1ad6eca8>
在比较时,它们被认为是相等的(==)。
虽然使用'is'操作符可以正确工作,但逐个比较这两个列表会非常耗时,因为它们可能很大,而且这个操作会重复很多次。
我在Python中对SWIG对象的理解有什么遗漏,以至于无法进行'=='和集合操作呢?
1 个回答
只要你的对象遵循了Python的对象协议,那么你关心的高级容器操作就会自动生效。为了展示这些操作是如何实现的,我们将重点讨论operator==
/__eq__
。
我们可以设置一个测试案例来研究Python和SWIG中的比较是如何工作的:
import test
f1 = test.foo(1)
f2 = test.foo(2)
f3 = test.foo(1)
f4 = test.static_foo()
f5 = test.static_foo()
a = (f1,f2,f3,f4,f5)
compared = "\n".join((",".join(str(int(y==x)) for y in a) for x in a))
print compared
print "\n".join((str(x) for x in a))
以我们简单的实现作为起点:
%module test
%inline %{
struct foo {
foo(const int v) : v(v) {}
int v;
};
foo *static_foo() {
static foo f{1};
return &f;
}
%}
运行后会得到:
1,0,0,0,0
0,1,0,0,0
0,0,1,0,0
0,0,0,1,0
0,0,0,0,1
<test.foo; proxy of <Swig Object of type 'foo *' at 0xb71e79f8> >
<test.foo; proxy of <Swig Object of type 'foo *' at 0xb71e7ad0> >
<test.foo; proxy of <Swig Object of type 'foo *' at 0xb71e7b78> >
<test.foo; proxy of <Swig Object of type 'foo *' at 0xb71e7b90> >
<test.foo; proxy of <Swig Object of type 'foo *' at 0xb71e7b60> >
这结果可真不怎么样,只是身份矩阵而已。
那么为什么会这样呢?首先,SWIG默认会为从C++返回到Python的每个对象构造一个新的代理对象。即使在静态情况下,SWIG在编译时也无法证明返回的对象总是相同的,所以为了安全起见,它总是会创建一个新的代理。
在运行时,我们可以添加一个类型映射来检查并处理这种情况(例如,使用一个std::map
来查找实例)。不过这是一个单独的问题,且会分散我们关注的重点,因为这并不会让f1==f3
成立,因为它们是不同但等价的对象。
在C++中,我们也面临同样的问题,但由于各种设计原因,我们甚至无法编译一个简单的使用operator==
的函数:
bool bar() {
static foo f1{2};
static foo f2{2};
return f1==f2;
}
编译失败,错误信息为:
test.cxx:6 error: no match for ‘operator==’ in ‘f1 == f2’
让我们深入探讨一下,以理解SWIG在生成Python包装时的行为。如果我们在C++类中添加一个operator==
:
%module test
%inline %{
struct foo {
foo(const int v) : v(v) {}
int v;
bool operator==(const foo& o) const { return v == o.v; }
};
foo *static_foo() {
static foo f{1};
return &f;
}
那么SWIG就会正确处理,并将其传递给Python,这样我们的测试案例现在会产生:
1,0,1,1,1
0,1,0,0,0
1,0,1,1,1
1,0,1,1,1
1,0,1,1,1
<test.foo; proxy of <Swig Object of type 'foo *' at 0xb72869f8> >
<test.foo; proxy of <Swig Object of type 'foo *' at 0xb7286ad0> >
<test.foo; proxy of <Swig Object of type 'foo *' at 0xb7286a70> >
<test.foo; proxy of <Swig Object of type 'foo *' at 0xb7286bc0> >
<test.foo; proxy of <Swig Object of type 'foo *' at 0xb7286bd8> >
这正是我们希望的行为,所有v
相同的实例都是相等的,其他的则不是。尽管每个实例的代理对象是不同的。
如果我们写的operator==
是一个非成员操作符,会发生什么呢?
%module test
%inline %{
struct foo {
foo(const int v) : v(v) {}
int v;
};
foo *static_foo() {
static foo f{1};
return &f;
}
bool operator==(const foo& a, const foo& b) { return a.v== b.v; }
突然间,我们失去了这种行为,结果又回到了:
1,0,0,0,0
0,1,0,0,0
0,0,1,0,0
0,0,0,1,0
0,0,0,0,1
为什么会这样?因为在这种情况下,如何处理operator==
并不明确。如果你用-Wall
运行SWIG,你会看到现在出现了一个警告:
test.i:15: Warning 503: Can't wrap 'operator ==' unless renamed to a valid identifier.
那么假设我们不能编辑C++代码,来看看如何“修复”这个问题。我们可以通过几种方式来做到这一点。
我们可以要求SWIG将它不知道如何包装的
operator==
重命名为一个函数,使用%rename
,正如警告所提示的那样:%rename(compare_foo) operator==(const foo&, const foo&);
这需要在SWIG看到
operator==
的声明/定义之前写入。但仅此并不足以恢复我们想要的行为,因此我们通过在SWIG的输出中添加一些额外的Python代码来修复它。请记住,Python函数
__eq__
的形式如下:object.__eq__(self, other)
这实际上与我们的C++
operator==
非常匹配,因此我们可以简单地在SWIG接口文件的末尾添加以下内容:%pythoncode %{ foo.__eq__ = lambda a,b: compare_foo(a,b) %}
这恢复了我们想要的行为。(注意:我不太确定为什么这里需要lambda,我没想到它是必需的)
我们也可以通过在接口中写一些额外的C++代码来做到这一点,而不需要修改我们正在包装的实际代码。基本上,我们想要做的是在
foo
内部实现__eq__
。这可以通过%extend
来实现,它从目标语言的角度扩展一个类。为了完整性,我们使用%ignore
来抑制我们收到警告的函数,因为我们已经处理了这个问题。%module test %ignore operator==(const foo&, const foo&); %extend foo { bool __eq__(const foo& o) const { return *$self == o; } } %inline %{ struct foo { foo(const int v) : v(v) {} int v; }; foo *static_foo() { static foo f{1}; return &f; } bool operator==(const foo& a, const foo& b) { return a.v== b.v; } bool bar() { static foo f1{2}; static foo f2{2}; return f1==f2; } %}
这同样恢复了我们想要的行为,区别在于粘合剂是作为C++粘合剂而不是Python粘合剂包含的。
最后,如果你在运行SWIG Python时使用
-builtin
,那么上述两种解决方案都不适用于非成员操作符的情况。值得注意的是,默认的SWIG输出包含一个tp_richcompare
函数。不过,要使用我们的operator==
而不是对底层对象的地址比较,你需要使用插槽机制来注册我们自己的函数,类似于上面的代码。