【问题标题】:How can I intercept calls to python's "magic" methods in new style classes?如何在新样式类中拦截对 python 的“魔术”方法的调用?
【发布时间】:2012-02-21 21:08:40
【问题描述】:

我正在尝试在新样式类中拦截对 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

如何使用新的样式类完成这种拦截?

【问题讨论】:

  • 拦截双下划线方法的真正目的是什么?还是只是为了好奇?
  • 我总是在此处保留所有魔法方法的列表:github.com/niccokunzmann/wwp/blob/…(已生成。因此适用于 python 2 和 3)
  • 实际上,您似乎想要做的是拦截对新样式类的 instances 的魔术方法的调用 - 但是恕我直言,这仍然是一个好问题。跨度>

标签: python python-3.x


【解决方案1】:

出于性能原因,Python 总是在类(和父类的)__dict__ 中查找魔术方法,并且不使用正常的属性查找机制。一种解决方法是在创建类时使用元类自动为魔术方法添加代理;例如,我使用这种技术来避免为包装类编写样板调用方法。

class Wrapper(object):
    """Wrapper class that provides proxy access to an instance of 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__

【讨论】:

  • @kindall 我删除了看起来像孤立的代码行,但您可能需要仔细检查您的示例是否没有失去预期的功能。
  • 在循环通过dir() 之前执行ignore.update(dct) 是否有任何问题,因此将最后两个if 语句压缩为一个?对我来说感觉更干净,但也许会有我没有经验的意外后果。
  • 在 Python 3.8 上,我发现这个示例实际上并不能正常工作。
【解决方案2】:

使用__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__ 方法仅在其他方法不起作用时才被调用(它不适用于运算符,您可以阅读 here 的相关信息)。

在您的示例中,__repr__ 和许多其他魔术方法已在 object 类中定义。

想一想,可以做一件事,就是定义那些神奇的方法,然后调用__getattr__方法。检查我的另一个问题及其答案 (link) 以查看执行此操作的代码。

【讨论】:

    【解决方案3】:

    根据Asymmetric behavior for __getattr__, newstyle vs oldstyle classes 的答案(另请参阅Python docs),用__getattr__ 或__getattribute__ 修改对“魔术”方法的访问对于新型类来说是不可能的。这种限制使解释器更快。

    【讨论】:

    • 谢谢。我发现 this answer 有助于充实您的答案。
    【解决方案4】:

    从文档中剪切和复制:

    对于旧式类,查找特殊方法的方式始终与查找任何其他方法或属性的方式完全相同。

    对于新型类,特殊方法的隐式调用只有在对象类型上定义时才能保证正常工作,而不是在对象的实例字典中。

    【讨论】:

    • 这个答案是 100% 正确的,但什么也没解释,完全没有帮助。对不起。
    • 贝尔实验室的一个老笑话:迷失在浓雾中的飞行员看到一栋办公楼,大喊“我在哪里?”,“在飞机上”得到答案,然后降落在.. . 阿伦敦。他怎么知道的? “答案是 100% 正确的……”
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2011-02-11
    • 2012-04-01
    • 1970-01-01
    • 2015-08-08
    • 2010-11-11
    相关资源
    最近更新 更多