python SWIG 对象比较

5 投票
1 回答
1789 浏览
提问于 2025-04-18 15:41

我有两个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 个回答

14

只要你的对象遵循了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++代码,来看看如何“修复”这个问题。我们可以通过几种方式来做到这一点。

  1. 我们可以要求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,我没想到它是必需的)

  2. 我们也可以通过在接口中写一些额外的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粘合剂包含的。

  3. 最后,如果你在运行SWIG Python时使用-builtin,那么上述两种解决方案都不适用于非成员操作符的情况。值得注意的是,默认的SWIG输出包含一个tp_richcompare函数。不过,要使用我们的operator==而不是对底层对象的地址比较,你需要使用插槽机制来注册我们自己的函数,类似于上面的代码。

撰写回答