Python 属性与 Swig

20 投票
6 回答
9670 浏览
提问于 2025-04-15 13:09

我正在尝试使用swig为一些C++代码创建Python绑定。看起来我在尝试从一些访问器函数创建Python属性时遇到了问题,这些函数是用来处理像下面这样的函数的方法:

class Player {
public:
  void entity(Entity* entity);
  Entity* entity() const;
};

我试着用Python的property函数来创建一个属性,但似乎swig生成的包装类与这个函数不兼容,至少在设置属性时是这样。

那么,使用swig该怎么创建属性呢?

6 个回答

22

使用 Attributes.i

在 SWIG Lib 文件夹里,有一个叫做 "attributes.i" 的文件,这个文件在文档里没有提到,但里面包含了一些内联文档。

你只需要在你的接口文件中添加以下一行代码。

%include <attributes.i>

这样你就可以获得一些宏(比如 %attribute),用来定义现有方法的属性。

以下是 attributes.i 文件中文档的摘录:

下面的宏可以把一对设置/获取方法转换成一个“原生”的属性。当你有一对获取/设置方法,并且它们的类型是基本类型时,就可以使用 %attribute,比如:

  %attribute(A, int, a, get_a, set_a);

  struct A
  {
    int get_a() const;
    void set_a(int aa);
  };
35

有一种简单的方法可以通过swig将python的方法转换为属性。
假设我们有一个C++代码文件 Example.h:

C++头文件

class Example{
    public:
      void SetX(int x);
      int  GetX() const;
    };

现在我们来把这个设置器和获取器转换成python的属性'x'。诀窍在于.i文件。我们需要添加一些“swiggy”的内联python代码(用%pythoncode),这些代码会被插入到生成的python类的主体中(在自动生成的python代码里)。

Swig包装 Example.i

%module example
%{
     #include "example.h"
%}

class Example{
    public:
      void SetX(int x);
      int  GetX() const;

      %pythoncode %{
         __swig_getmethods__["x"] = GetX
         __swig_setmethods__["x"] = SetX
         if _newclass: x = property(GetX, SetX)
      %}
    };

看看生成的python代码:

python测试代码

import example

test = example.Example()
test.x = 5
print "Ha ha ha! It works! X = ", repr(test.x)

就这样!



让它更简单!

其实不需要重写类的定义。多亏了Joshua的建议,我们可以使用SWIG指令%extend ClassName { }。

Swig包装 Example.i

%module example
%{
     #include "example.h"
%}

%extend Example{
      %pythoncode %{
         __swig_getmethods__["x"] = GetX
         __swig_setmethods__["x"] = SetX
         if _newclass: x = property(GetX, SetX)
      %}
    };

隐藏设置器和获取器函数

如你所见,转换后test.GetX()和test.SetX()仍然存在。我们可以通过以下方式隐藏它们:

a) 使用%rename重命名函数,在前面加上'_',这样就可以让这些方法在python中变成“私有”的。在SWIG接口.i文件中:

...
class Example{
   %rename(_SetX) SetX(int);
   %rename(_GetX) GetX();
...

(%rename可以放在其他地方,以便于将这个类转换为其他语言,这些语言不需要这些'_')

b) 或者可以使用%feature("shadow")来处理。

为什么要这样做?

为什么我们必须使用这些方法通过SWIG将方法转换为属性呢?正如所说,SWIG自私地覆盖了_setattr_,所以我们必须使用_swig_getmethods__swig_setmethods_来注册函数,并保持在swig的方式中。

为什么有人会更喜欢这种方式?

上面提到的方法,尤其是使用PropertyVoodoo的方式,就像是为了煎一个蛋而烧掉房子一样。此外,这种方法会破坏类的布局,因为我们必须创建继承类才能从C++方法中生成python属性。比如说,如果类Cow返回类Milk,而继承类是MilkWithProperties(Milk),那么如何让Cow生成MilkWithProperties呢?

这种方法允许我们:

  1. 明确控制哪些C++方法转换为python属性
  2. 转换规则位于swig接口(*.i)文件中,这正是它们应该在的地方
  3. 生成一个自动生成的.py文件
  4. 保持在swig生成的.py文件中的插入语法
  5. %pythoncode在将库包装到其他语言时会被忽略

更新 在新版本中,SWIG放弃了_swig_property,所以直接使用property。这在旧版本的swig中也能正常工作。我已经更新了这篇文章。

4

哦,这个问题有点棘手(但也很有趣)。SWIG 并不把这个当作生成@property的机会:我想如果不小心的话,很容易就会误判出很多错误的情况。不过,由于SWIG在生成C++代码时不会这样做,但在Python中,我们完全可以通过一个小的元类来实现。

接下来,假设我们有一个Math类,可以设置和获取一个名为“pi”的整数变量。然后我们可以使用以下代码:

example.h

#ifndef EXAMPLE_H
#define EXAMPLE_H

class Math {
 public:
    int pi() const {
        return this->_pi;
    }

    void pi(int pi) {
        this->_pi = pi;
    }

 private:
    int _pi;
};

#endif

example.i

%module example

%{
    #define SWIG_FILE_WITH_INIT
    #include "example.h"
%}

[essentially example.h repeated again]

example.cpp

#include "example.h"

util.py

class PropertyVoodoo(type):
    """A metaclass. Initializes when the *class* is initialized, not
    the object. Therefore, we are free to muck around the class
    methods and, specifically, descriptors."""

    def __init__(cls, *a):
        # OK, so the list of C++ properties using the style described
        # in the OP is stored in a __properties__ magic variable on
        # the class.
        for prop in cls.__properties__:

            # Get accessor.
            def fget(self):
                # Get the SWIG class using super. We have to use super
                # because the only information we're working off of is
                # the class object itself (cls). This is not the most
                # robust way of doing things but works when the SWIG
                # class is the only superclass.
                s = super(cls, self)

                # Now get the C++ method and call its operator().
                return getattr(s, prop)()

            # Set accessor.
            def fset(self, value):
                # Same as above.
                s = super(cls, self)

                # Call its overloaded operator(int value) to set it.
                return getattr(s, prop)(value)

            # Properties in Python are descriptors, which are in turn
            # static variables on the class. So, here we create the
            # static variable and set it to the property.
            setattr(cls, prop, property(fget=fget, fset=fset))

        # type() needs the additional arguments we didn't use to do
        # inheritance. (Parent classes are passed in as arguments as
        # part of the metaclass protocol.) Usually a = [<some swig
        # class>] right now.
        super(PropertyVoodoo, cls).__init__(*a)

        # One more piece of work: SWIG selfishly overrides
        # __setattr__. Normal Python classes use object.__setattr__,
        # so that's what we use here. It's not really important whose
        # __setattr__ we use as long as we skip the SWIG class in the
        # inheritance chain because SWIG's __setattr__ will skip the
        # property we just created.
        def __setattr__(self, name, value):
            # Only do this for the properties listed.
            if name in cls.__properties__:
                object.__setattr__(self, name, value)
            else:
                # Same as above.
                s = super(cls, self)

                s.__setattr__(name, value)

        # Note that __setattr__ is supposed to be an instance method,
        # hence the self. Simply assigning it to the class attribute
        # will ensure it's an instance method; that is, it will *not*
        # turn into a static/classmethod magically.
        cls.__setattr__ = __setattr__

somefile.py

import example
from util import PropertyVoodoo

class Math(example.Math):
    __properties__ = ['pi']
    __metaclass__  = PropertyVoodoo

m = Math()
print m.pi
m.pi = 1024
print m.pi
m.pi = 10000
print m.pi

最终的结果就是,你需要为每个SWIG的Python类创建一个包装类,然后写两行代码:一行用来标记哪些方法应该转换为属性,另一行用来引入元类。

撰写回答