Python 属性和 Swig

Posted

技术标签:

【中文标题】Python 属性和 Swig【英文标题】:Python Properties & Swig 【发布时间】:2010-11-14 02:22:39 【问题描述】:

我正在尝试使用 swig 为一些 C++ 代码创建 python 绑定。我似乎遇到了一个问题,试图从我拥有的一些访问器函数创建 python 属性,如下所示:

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

我尝试使用 python 属性函数创建一个属性,但似乎 swig 生成的包装类与它不兼容,至少对于 setter 而言。

如何使用 swig 创建属性?

【问题讨论】:

【参考方案1】:

有一种简单的方法可以从 swig 方法中创建 python 属性。 假设 C++ 代码 Example.h:

C++ 标头

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

让我们将此 setter 和 getter 转换为 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)
      %
    ;

隐藏 setter 和 getter 函数

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

a) 使用 %rename 重命名函数,在开头添加“_”,从而使 python 的方法“私有”。在 SWIG 界面 .i 例子.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?

这种方法允许人们:

    明确控制要转换为 python 属性的 C++ 方法 转换规则位于 swig interface(*.i) 文件中,它们应该在的地方 一个自动生成的 .py 文件 在 swig 生成的 .py 文件中保持插入的 swig 语法 如果将库包装成其他语言,则忽略%pythoncode

更新 在较新的版本中,SWIG 放弃了 _swig_property,因此只需使用 property。它与旧版本的 swig 相同。我已经换了帖子。

【讨论】:

谢谢!这似乎工作得很好。如果其他人觉得它有帮助,请记住,当类在单独的头文件中定义时,您可以使用 swig %extend ClassName 从接口文件中向类添加方法。我相信,当您重载具有相同名称的 C++ getter 和 setter 时,这种方法甚至会起作用。 下次我需要做一些 swig 和 python 时,我将不得不尝试你的答案。如果它确实像宣传的那样工作,这更像是我正在寻找的解决方案。 _newclass 是做什么的? 我记得它与旧的 python 兼容性有关(比如从 2​​.4 到 2.6)。在 python 2.6 中引入了当前形式的属性(python 2.4 有自制的带有属性的库)。因此,如果您使用现代 python(2.7、3.x),您可以跳过它。不过我可能是错的。【参考方案2】:

使用 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);
  ;

【讨论】:

在最新版本的 SWIG 中应该是 %include &lt;attribute.i&gt; 这是迄今为止一种干净的方法。此方法适用于聚合类型吗?如果不知道如何将 std::vector 作为 C++ 类中的成员作为属性访问?【参考方案3】:

哦,这很棘手(而且很有趣)。 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 类创建一个包装器类,然后键入两行:一行用于标记应在属性中转换哪些方法,另一行用于引入元类。

【讨论】:

这看起来应该可以解决问题!遗憾的是 SWIG 没有更直接的机制来生成属性。 是否有任何声明语法可以识别为属性? getPropName/setPropName 可能吗? 看起来 SWIG 并没有尝试生成这些,尽管我只花了大约一个小时左右的 SWIG 文档来编写这个答案的代码,而且完全有可能有一种我没有的方法遇到。 Hao Lian 的回答非常好,但是如果 properties 列表中有多个条目,则特定的 PropertyVoodoo 代码似乎会失败。我已经将该代码直接插入到我的 SWIG 输入文件中的 %pythoncode 块中,并且似乎接近解决这个非常烦人的问题。【参考方案4】:

Hao 的 ProperyVoodoo 元类的问题在于,当属性列表中有多个属性时,所有属性的行为都与列表中的最后一个相同。例如,如果我有一个列表或属性名称 ["x", "y", "z"],那么为所有三个生成的属性将使用与 "z" 相同的访问器。

经过一些实验,我相信我已经确定这个问题是由 Python 处理闭包的方式引起的(即嵌套函数中的名称引用包含范围内的变量)。要解决此问题,您需要将属性名称变量的本地副本获取到 fget 和 fset 方法中。使用默认参数很容易将它们潜入:

# (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 引用的字符串)。

【讨论】:

【参考方案5】:

我遇到了同样的问题,使用 %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()。【参考方案6】:

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

%extend 指令的一个鲜为人知的特性是它也可以 用于添加合成属性或修改 现有数据属性。例如,假设您想制作 量级 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);

%

【讨论】:

以上是关于Python 属性和 Swig的主要内容,如果未能解决你的问题,请参考以下文章

指向 swig(或 Boost::Python)中的成员的指针

扩展 SWIG 内置类

使用 SWIG 为 python2 和 python3 创建一个模块

python和swig版本兼容性问题

SWIG+c+Python:传递和接收 c 数组

Python:SWIG 与 ctypes