扩展SWIG内置类

6 投票
2 回答
965 浏览
提问于 2025-04-16 20:13

SWIG的-builtin选项有一个好处,就是运行速度更快,而且避免了多重继承时出现的一个错误。
不过,它的缺点是我不能在生成的类或任何子类上设置属性:
- 我可以很轻松地通过子类化来扩展Python的内置类型,比如列表(list):

class Thing(list):
    pass

Thing.myattr = 'anything' # No problem

- 但是如果我用同样的方法去处理SWIG的内置类型,就会出现以下问题:

class Thing(SWIGBuiltinClass):
    pass

Thing.myattr = 'anything'

AttributeError: type object 'Thing' has no attribute 'myattr'

我该如何解决这个问题呢?

2 个回答

0

这个问题源于swig如何实现"-builtin"中的类,使它们看起来像内置类(所以叫这个名字)。

内置类是不能扩展的——试着添加或修改“str”的某个成员,Python会不让你修改属性字典。

我有一个解决方案,已经用了好几年。

我不确定我能否推荐这个方法,因为:

  1. 这可以说是有点邪门——就像在C/C++中抛弃常量一样。
  2. 这个方法没有官方支持,未来的Python版本可能会坏掉。
  3. 我还没有在Python3上试过。
  4. 在生产代码中使用这种“黑魔法”让我有点不安——它可能会出问题,而且确实很晦涩——但至少有一家大公司在生产代码中使用了这个方法。

不过……我很喜欢它能很好地解决我们在调试时想要的一些奇怪功能。

这个想法不是我原创的,我是从这里得到的:https://gist.github.com/mahmoudimus/295200,作者是Mahmoud Abdelkader。

基本的想法是把swig创建的类型对象中的常量字典当作非常量字典来访问,然后添加或覆盖任何想要的方法。

顺便说一下,运行时修改类的技术叫做猴子补丁(monkeypatching),可以查看这个链接了解更多:https://en.wikipedia.org/wiki/Monkey_patch

首先——这是“monkeypatch.py”:

''' monkeypatch.py:
I got this from https://gist.github.com/mahmoudimus/295200 by Mahmoud Abdelkader,
his comment: "found this from Armin R. on Twitter, what a beautiful gem ;)"
I made a few changes for coding style preferences
- Rudy Albachten   April 30 2015
'''

import ctypes
from types import DictProxyType, MethodType

# figure out the size of _Py_ssize_t
_Py_ssize_t = ctypes.c_int64 if hasattr(ctypes.pythonapi, 'Py_InitModule4_64') else ctypes.c_int

# python without tracing
class _PyObject(ctypes.Structure):
    pass
_PyObject._fields_ = [
    ('ob_refcnt', _Py_ssize_t),
    ('ob_type', ctypes.POINTER(_PyObject))
]

# fixup for python with tracing
if object.__basicsize__ != ctypes.sizeof(_PyObject):
    class _PyObject(ctypes.Structure):
        pass
    _PyObject._fields_ = [
        ('_ob_next', ctypes.POINTER(_PyObject)),
        ('_ob_prev', ctypes.POINTER(_PyObject)),
        ('ob_refcnt', _Py_ssize_t),
        ('ob_type', ctypes.POINTER(_PyObject))
    ]

class _DictProxy(_PyObject):
    _fields_ = [('dict', ctypes.POINTER(_PyObject))]

def reveal_dict(proxy):
    if not isinstance(proxy, DictProxyType):
        raise TypeError('dictproxy expected')
    dp = _DictProxy.from_address(id(proxy))
    ns = {}
    ctypes.pythonapi.PyDict_SetItem(ctypes.py_object(ns), ctypes.py_object(None), dp.dict)
    return ns[None]

def get_class_dict(cls): 
    d = getattr(cls, '__dict__', None)
    if d is None:
        raise TypeError('given class does not have a dictionary')
    if isinstance(d, DictProxyType):
        return reveal_dict(d)
    return d

def test():
    import random
    d = get_class_dict(str)
    d['foo'] = lambda x: ''.join(random.choice((c.upper, c.lower))() for c in x)
    print "and this is monkey patching str".foo()

if __name__ == '__main__':
    test()

这里有一个使用猴子补丁的例子:

我在模块“mystuff”中有一个用swig包装的类“myclass”。

我想添加一个额外的运行时方法“namelen”,它返回myclass.getName()返回的名字的长度。

import mystuff
import monkeypatch

# add a "namelen" method to all "myclass" objects
def namelen(self):
    return len(self.getName())
d = monkeypatch.get_class_dict(mystuff.myclass)
d['namelen'] = namelen

x = mystuff.myclass("xxxxxxxx")
print "namelen:", x.namelen()

注意,这也可以用来扩展或覆盖内置Python类的方法,正如在monkeypatch.py中的测试所示:它给内置的str类添加了一个方法“foo”,返回一个原始字符串的副本,字母大小写随机。

我可能会把:

# add a "namelen" method to all "myclass" objects
def namelen(self):
    return len(self.getName())
d = monkeypatch.get_class_dict(mystuff.myclass)
d['namelen'] = namelen

替换为

# add a "namelen" method to all "myclass" objects
monkeypatch.get_class_dict(mystuff.myclass)['namelen'] = lambda self: return len(self.getName())

以避免额外的全局变量。

4

我偶然间找到了解决办法。我在尝试使用元类,想着能否在子类中重写内置类型的setattrgetattr函数。

在这个过程中,我发现内置类型已经有一个元类(SwigPyObjectType),所以我的元类必须继承这个元类。

就这样,这个方法就解决了问题。如果有人能解释一下为什么这样做有效,我会很高兴:

SwigPyObjectType = type(SWIGBuiltinClass)

class Meta(SwigPyObjectType):
    pass

class Thing(SWIGBuiltinClass):
    __metaclass__ = Meta

Thing.myattr = 'anything' # Works fine this time

撰写回答