如何在不出现无限递归错误的情况下实现__getattribute__?

140

我想在一个类中重写对一个变量的访问,但是正常返回其他所有变量。如何使用__getattribute__实现这个目标?

我尝试了下面的方法(也说明了我的尝试),但是我得到了递归错误:

class D(object):
    def __init__(self):
        self.test=20
        self.test2=21
    def __getattribute__(self,name):
        if name=='test':
            return 0.
        else:
            return self.__dict__[name]

>>> print D().test
0.0
>>> print D().test2
...
RuntimeError: maximum recursion depth exceeded in cmp
6个回答

157
因为在__getattribute__中尝试访问self.__dict__属性会再次调用__getattribute__导致出现递归错误。如果使用object__getattribute__则可以解决问题。
class D(object):
    def __init__(self):
        self.test=20
        self.test2=21
    def __getattribute__(self,name):
        if name=='test':
            return 0.
        else:
            return object.__getattribute__(self, name)

这个做法有效是因为 object(在这个例子中)是基类。通过调用基类版本的__getattribute__,您可以避免之前陷入的递归问题。

在foo.py中使用ipython输出的代码:

In [1]: from foo import *

In [2]: d = D()

In [3]: d.test
Out[3]: 0.0

In [4]: d.test2
Out[4]: 21

更新:

在当前文档的更多新式类别的属性访问部分中,有一个建议正是为了避免无限递归而进行的操作。


1
有趣。那你在做什么呢?为什么对象会有我的变量? - Greg
1
我是否总是想使用对象?如果我继承其他类,会怎样呢? - Greg
1
Egil:这并不是一个完全安全的观点。如果我在使用继承另一个类的类,那个类又继承了另一个类,而那个类又继承了对象(而且我没有编写任何继承的类)。如果其中一个重写了__getattribute__并且你调用了对象的版本,它可能无法正常工作... - Oli
30
使用super() 并在基类中使用第一个 getattribute 方法,这样做不是更好吗? - super(D, self).__getattribute__(name) - gepatino
24
在Python 3中,您可以使用super().__getattribute__(name)来实现该功能。 - jeromej
显示剩余11条评论

35

实际上,我认为您想使用特殊方法__getattr__

引用自Python文档:

__getattr__( self, name)

在常规查找位置没有找到属性(即它不是实例属性,也没有在 self 的类树中找到)时调用。name 是属性名。此方法应返回(计算出的)属性值或引发AttributeError异常。
请注意,如果通过正常机制找到属性,则不会调用__getattr__()。(这是__getattr__()__setattr__()之间有意的不对称性。)出于效率原因以及因为否则__setattr__()将无法访问实例的其他属性,因此执行此操作。请注意,至少对于实例变量,您可以通过不在实例属性字典中插入任何值(而是将它们插入另一个对象中)来伪造总控制权。有关在新样式类中实际获取完全控制权的方法,请参见下面的__getattribute__()方法。

注意:要使其起作用,实例不应该具有test属性,因此应删除self.test = 20这一行。


4
根据原始代码的本质,重写 test__getattr__ 是无用的,因为它总是能够在“通常的位置”找到它。 - Casey Kuball

21

Python语言参考:

为了避免此方法中的无限递归,在其实现中应始终使用相同名称调用基类方法来访问所需的任何属性,例如,object.__getattribute__(self, name)

含义:

def __getattribute__(self,name):
    ...
        return self.__dict__[name]

您正在调用名为__dict__的属性。由于它是一个属性,因此会调用__getattribute__来搜索__dict____getattribute__又会调用...之类的操作。

return  object.__getattribute__(self, name)

使用基类__getattribute__可以帮助找到真正的属性。


14

如何使用__getattribute__方法?

在正常的点语法查找之前调用此方法。如果引发AttributeError异常,则会调用__getattr__方法。

这种方法的使用相当罕见。标准库中只有两个定义:

$ grep -Erl  "def __getattribute__\(self" cpython/Lib | grep -v "/test/"
cpython/Lib/_threading_local.py
cpython/Lib/importlib/util.py

最佳实践

通过property函数来编写程序控制单个属性的正确方式。类D应按以下方式编写(设置器和删除器可选地复制表面上预期的行为):

class D(object):
    def __init__(self):
        self.test2=21

    @property
    def test(self):
        return 0.

    @test.setter
    def test(self, value):
        '''dummy function to avoid AttributeError on setting property'''

    @test.deleter
    def test(self):
        '''dummy function to avoid AttributeError on deleting property'''

使用方法:

>>> o = D()
>>> o.test
0.0
>>> o.test = 'foo'
>>> o.test
0.0
>>> del o.test
>>> o.test
0.0

属性是数据描述符,因此在正常的点查找算法中首先查找。

__getattribute__的选项

如果您绝对需要通过__getattribute__实现每个属性的查找,则有以下几种选项。

  • 引发AttributeError,从而调用__getattr__(如果已实现)
  • 通过以下方式之一从其中返回内容:
    • 使用super调用父级(可能是object的)实现
    • 调用__getattr__
    • 以某种方式实现自己的点查找算法

例如:

class NoisyAttributes(object):
    def __init__(self):
        self.test=20
        self.test2=21
    def __getattribute__(self, name):
        print('getting: ' + name)
        try:
            return super(NoisyAttributes, self).__getattribute__(name)
        except AttributeError:
            print('oh no, AttributeError caught and reraising')
            raise
    def __getattr__(self, name):
        """Called if __getattribute__ raises AttributeError"""
        return 'close but no ' + name    


>>> n = NoisyAttributes()
>>> nfoo = n.foo
getting: foo
oh no, AttributeError caught and reraising
>>> nfoo
'close but no foo'
>>> n.test
getting: test
20

你最初想要的是什么

以下示例展示了如何实现你最初想要的内容:

class D(object):
    def __init__(self):
        self.test=20
        self.test2=21
    def __getattribute__(self,name):
        if name=='test':
            return 0.
        else:
            return super(D, self).__getattribute__(name)

并且将会表现得像这样:

>>> o = D()
>>> o.test = 'foo'
>>> o.test
0.0
>>> del o.test
>>> o.test
0.0
>>> del o.test

Traceback (most recent call last):
  File "<pyshell#216>", line 1, in <module>
    del o.test
AttributeError: test

代码评审

您的代码附有注释。您在__getattribute__中使用了self的点式查找,这就是为什么会出现递归错误的原因。您可以检查name是否为"__dict__"并使用super进行解决,但这并不能涵盖__slots__。我将把这留给读者作为练习。

class D(object):
    def __init__(self):
        self.test=20
        self.test2=21
    def __getattribute__(self,name):
        if name=='test':
            return 0.
        else:      #   v--- Dotted lookup on self in __getattribute__
            return self.__dict__[name]

>>> print D().test
0.0
>>> print D().test2
...
RuntimeError: maximum recursion depth exceeded in cmp

13

你确定要使用__getattribute__吗? 你实际上想要实现什么?

最简单的方法是:

class D(object):
    def __init__(self):
        self.test = 20
        self.test2 = 21

    test = 0

或者:

class D(object):
    def __init__(self):
        self.test = 20
        self.test2 = 21

    @property
    def test(self):
        return 0

编辑: 请注意,D的实例在每种情况下都会有不同的test值。在第一种情况下,d.test将为20,在第二种情况下,它将为0。我将让您自己想出原因。

编辑2: Greg指出,第二个示例将失败,因为该属性是只读的,而__init__方法尝试将其设置为20。更完整的示例如下:

class D(object):
    def __init__(self):
        self.test = 20
        self.test2 = 21

    _test = 0

    def get_test(self):
        return self._test

    def set_test(self, value):
        self._test = value

    test = property(get_test, set_test)

显然,作为一个类,这几乎完全没用,但它可以给你提供一个跳板来继续前进。


哦,但是运行类时似乎并不起作用,对吧? File "Script1.py", line 5, in init self.test = 20 AttributeError: 无法设置属性 - Greg
没错。我会将其作为第三个例子修正。你很敏锐。 - Singletoned

5
这里有一个更可靠的版本:
class D(object):
    def __init__(self):
        self.test = 20
        self.test2 = 21
    def __getattribute__(self, name):
        if name == 'test':
            return 0.
        else:
            return super(D, self).__getattribute__(name)

它从父类调用__getattribute__方法,最终回退到object.__getattribute__方法,如果其他祖先没有覆盖它。


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