Python属性和Swig

20

我正在尝试使用swig为一些C++代码创建python绑定。我似乎在尝试从一些访问器函数中创建python属性时遇到了问题,这些函数是用于类似以下方法的方法:

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

我尝试使用Python的property函数创建属性,但似乎Swig生成的包装类与它不兼容,至少在设置器方面如此。

您如何使用Swig创建属性?

6个回答

35

有一种简单的方法可以使用 SWIG 从方法中创建 Python 属性。
假设有以下 C++ 代码 Example.h:

C++ 头文件

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

让我们将这个setter和getter转换为Python的property 'x'。诀窍在于.i文件中。我们添加一些"swiggy"内联Python代码(使用%pythoncode),该代码插入到生成的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)
      %}
    };

隐藏 getter 和 setter 函数

正如你所看到的,即使进行了转换,test.GetX() 和 test.SetX() 仍然存在。可以通过以下方式将它们隐藏起来:

a) 使用 %rename 重命名函数,在函数名前加上 '_',从而使方法对于 Python “私有”。在 SWIG 接口文件 Example.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文件中插入swig语法
  5. 如果将库包装到其他语言中,则忽略%pythoncode

更新 在更新版本的SWIG中放弃了_swig_property,因此可以使用property。它与旧版本的SWIG相同。我已经更新了文章。


3
谢谢!这似乎非常有效。如果其他人也觉得有帮助,请记住,使用swig,您可以在接口文件中使用%extend ClassName {}来向类添加方法,即使该类在单独的头文件中定义。我相信即使您将C ++ getter和setter重载为相同名称,这种方法也可以正常工作。 - Joshua
下次我需要使用Swig和Python时,我将不得不尝试您的答案。如果它确实像广告中所说的那样工作,那么这更接近我正在寻找的解决方案。 - fuzzy-waffle
_newclass 是用来做什么的? - Dave
据我回忆,这与旧版Python的兼容性有关(例如从2.4到2.6)。当前形式下的属性之类的东西是在Python 2.6中引入的(Python 2.4具有自制属性库)。因此,如果您正在使用现代Python(2.7、3.x),则可以跳过它。不过,我的记忆也可能有误。 - MajesticRa

22

使用Attributes.i

在SWIG Lib文件夹中有一个名为 "attributes.i" 的文件,该文件未在文档中讨论,但包含内联文档。

你只需要将以下行添加到你的接口文件即可。

%include <attributes.i>

然后您会收到一些宏(例如%attribute),用于从现有方法中定义属性。

来自attributes.i文件文档的摘录:

以下宏将一对set/get方法转换为“本地”属性。当您拥有一对与原始类型类似的get/set方法时,请使用%attribute:

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

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

5
最新版的SWIG应该包含%include <attribute.i> - Homar
1
这绝对是一种干净的方法。这种方法适用于聚合类型吗?如果不是,有什么办法可以将C++类中的std::vector作为属性访问? - Imran

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 类创建一个包装器类,然后输入两行代码:一行标记应转换为属性的方法,另一行引入元类。

1
这看起来应该可以解决问题!遗憾的是,SWIG没有更直接的机制来创建属性。 - fuzzy-waffle
有没有任何声明语法可以识别为属性?例如getPropName/setPropName? - Toji
看起来SWIG并没有尝试生成那些,虽然我只花了大约一个小时的时间阅读SWIG文档来编写这个答案的代码,但完全有可能有我没有发现的方法。 - hao
好莲的回答很棒,但是如果__properties__列表中有多个条目,则特定的PropertyVoodoo代码似乎会失败。我已经将该代码直接插入到我的SWIG输入文件的%pythoncode块中,并且似乎已经接近解决这个非常恼人的问题了。 - user257630

4
Hao的PropertyVoodoo元类存在一个问题,当属性列表中有多个属性时,所有属性的行为都与列表中的最后一个属性相同。例如,如果我有一个属性名称列表["x","y","z"],那么生成的所有三个属性将使用与“z”相同的访问器。
经过一些实验,我认为这个问题是由Python处理闭包的方式引起的(即,在嵌套函数中引用包含作用域中变量的名称)。要解决这个问题,需要在fget和fset方法中获取属性名称变量的本地副本。使用默认参数很容易将它们 sneaking in。
# (NOTE: Hao's comments removed for brevity)
class PropertyVoodoo(type):

def __init__(cls, *a):

    for prop in cls.__properties__:

        def fget(self, _prop = str(prop)):
            s = super(cls, self)
            return getattr(s, _prop)()


        def fset(self, value, _prop = str(prop)):
            s = super(cls, self)
            return getattr(s, _prop)(value)

        setattr(cls, prop, property(fget=fget, fset=fset))

    super(PropertyVoodoo, cls).__init__(*a)

    def __setattr__(self, name, value):
        if name in cls.__properties__:
            object.__setattr__(self, name, value)
        else:
            s = super(cls, self)
            s.__setattr__(name, value)

    cls.__setattr__ = __setattr__

请注意,给fget和fset方法添加额外的_prop参数是完全安全的,因为property()类永远不会直接向它们传递值,这意味着它们将始终是默认值(即每个fget和fset方法创建时prop引用的字符串的副本)。

1

来自http://www.swig.org/Doc2.0/SWIGDocumentation.html#SWIG_adding_member_functions

%extend指令的一个鲜为人知的特性是它也可以用于添加合成属性或修改现有数据属性的行为。例如,假设您想要将magnitude作为Vector的只读属性而不是方法。

因此,在您的示例中,以下内容应该有效:

%extend Player {
    Entity entity;
}

%{
Entity* Player_entity_get(Player* p) {
  return p->get_entity();
}
void Player_entityProp_set(Player* p, Entity* e) {
  p->set_entity(e);
}
%}

1

我也遇到了同样的问题,使用%pythoncode的建议对我很有帮助。这是我所做的:

class Foo {
  // ...
  std::string get_name();
  bool set_name(const std::string & name);
};

在包装器中:

%include "foo.h"
%pythoncode %{
def RaiseExceptionOnFailure(mutator):
  def mutator(self, v):
    if not mutator(self, v):
     raise ValueError("cannot set property")
  return wrapper
Foo.name = property(Foo.get_name, RaiseExceptionOnFailure(Foo.set_name))
%}

如果您正在创建只读属性,请不要将第二个参数传递给property()。 - GaryO

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接