如何拦截新式类中Python的“魔法”方法调用?

37 投票
4 回答
13773 浏览
提问于 2025-04-17 11:55

我正在尝试拦截对Python中双下划线魔法方法的调用,这些方法通常在新式类中使用。这是一个简单的例子,但能说明我的意图:

class ShowMeList(object):
    def __init__(self, it):
        self._data = list(it)

    def __getattr__(self, name):
        attr = object.__getattribute__(self._data, name)
        if callable(attr):
            def wrapper(*a, **kw):
                print "before the call"
                result = attr(*a, **kw)
                print "after the call"
                return result
            return wrapper
        return attr

如果我在列表周围使用那个代理对象,对于非魔法方法,我得到了预期的行为,但我的包装函数从未被调用过,对于魔法方法来说。

>>> l = ShowMeList(range(8))

>>> l #call to __repr__
<__main__.ShowMeList object at 0x9640eac>

>>> l.append(9)
before the call
after the call

>> len(l._data)
9

如果我不从对象继承(第一行 class ShowMeList:),一切都按预期工作:

>>> l = ShowMeList(range(8))

>>> l #call to __repr__
before the call
after the call
[0, 1, 2, 3, 4, 5, 6, 7]

>>> l.append(9)
before the call
after the call

>> len(l._data)
9

我该如何在新式类中实现这个拦截呢?

4 个回答

4

根据对__getattr__的非对称行为,新式类与旧式类的回答(还可以参考Python文档),在新式类中,不能通过__getattr____getattribute__来修改对“魔法”方法的访问。这种限制让解释器的运行速度更快。

6

使用 __getattr____getattribute__ 是一个类在获取属性时的最后手段。

考虑以下内容:

>>> class C:
    x = 1
    def __init__(self):
        self.y = 2
    def __getattr__(self, attr):
        print(attr)

>>> c = C()
>>> c.x
1
>>> c.y
2
>>> c.z
z

当其他方法都无法工作时,__getattr__ 方法才会被调用(它对运算符不起作用,具体可以查看 这里)。

在你的例子中,__repr__ 和许多其他魔法方法已经在 object 类中定义好了。

不过,有一件事可以做,那就是定义这些魔法方法,并让它们调用 __getattr__ 方法。可以查看我提的另一个问题及其回答(链接),里面有一些相关的代码示例。

34

为了提高性能,Python在查找特殊方法时,总是直接查看类及其父类的__dict__,而不是使用普通的属性查找方式。为了绕过这个限制,可以使用元类,在类创建时自动为特殊方法添加代理;我用过这种方法,避免了为包装类写很多重复的调用方法。

class Wrapper(object):
    """Wrapper class that provides proxy access to some internal instance."""

    __wraps__  = None
    __ignore__ = "class mro new init setattr getattr getattribute"

    def __init__(self, obj):
        if self.__wraps__ is None:
            raise TypeError("base class Wrapper may not be instantiated")
        elif isinstance(obj, self.__wraps__):
            self._obj = obj
        else:
            raise ValueError("wrapped object must be of %s" % self.__wraps__)

    # provide proxy access to regular attributes of wrapped object
    def __getattr__(self, name):
        return getattr(self._obj, name)
    
    # create proxies for wrapped object's double-underscore attributes
    class __metaclass__(type):
        def __init__(cls, name, bases, dct):

            def make_proxy(name):
                def proxy(self, *args):
                    return getattr(self._obj, name)
                return proxy

            type.__init__(cls, name, bases, dct)
            if cls.__wraps__:
                ignore = set("__%s__" % n for n in cls.__ignore__.split())
                for name in dir(cls.__wraps__):
                    if name.startswith("__"):
                        if name not in ignore and name not in dct:
                            setattr(cls, name, property(make_proxy(name)))

用法:

class DictWrapper(Wrapper):
    __wraps__ = dict

wrapped_dict = DictWrapper(dict(a=1, b=2, c=3))

# make sure it worked....
assert "b" in wrapped_dict                        # __contains__
assert wrapped_dict == dict(a=1, b=2, c=3)        # __eq__
assert "'a': 1" in str(wrapped_dict)              # __str__
assert wrapped_dict.__doc__.startswith("dict()")  # __doc__

撰写回答