如何实现__getattribute__而没有无限递归错误?


100

我想覆盖对类中一个变量的访问,但通常返回所有其他变量。我该怎么做__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

Answers:


126

您收到递归错误,因为您尝试访问其中的self.__dict__属性会再次__getattribute__调用您__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__可以避免您以前遇到的递归地狱。

IPython的输出与foo.py中的代码:

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
有趣。那你在那做什么 为什么对象会有我的变量?
格雷格,

1
..我想一直使用对象吗?如果我从其他类继承怎么办?
格雷格,

1
是的,每次创建一个类而不编写自己的类时,都使用object提供的getattribute
Egil

19
使用super()并因此使用python在基类中找到的第一个getattribute方法更好吗?-super(D, self).__getattribute__(name)
gepatino

10
或者你也可以使用super().__getattribute__(name)在Python 3
jeromej

25

实际上,我相信您想改用__getattr__特殊方法。

引用Python文档:

__getattr__( self, name)

当在常规位置未找到属性时调用该属性(即,它不是实例属性,也不是在自身的类树中找到该属性)。name是属性名称。此方法应返回(计算出的)属性值或引发AttributeError异常。
请注意,如果通过常规机制找到该属性,__getattr__()则不会调用该属性。(这是__getattr__()和之间的故意不对称__setattr__()。)这样做是出于效率方面的考虑,并且因为否则__setattr__()将无法访问实例的其他属性。请注意,至少对于实例变量,您可以通过不在实例属性字典中插入任何值(而是将其插入另一个对象中)来伪造总体控制。见__getattribute__() 方法,以实际获得新样式类中的总控制权。

注:对于这项工作,该实例应该不会有一个test属性,因此行self.test=20应该被删除。


2
实际上,根据OP代码的性质,压倒一切__getattr__test将是无用的,因为它总能找到“在平时的地方”吧。
Casey Kuball 2012年

1
当前指向相关Python文档的链接(似乎与答案中引用的链接不同):docs.python.org/3/reference/datamodel.html#object.__getattr__
CrepeGoat

17

Python语言参考:

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

含义:

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

您正在调用名为的属性__dict__。由于它是一个属性,因此__getattribute__会在搜索__dict__中调用__getattribute__哪个调用而被调用... yada yada yada

return  object.__getattribute__(self, name)

使用基类__getattribute__有助于查找真实属性。


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。我将由您自己确定原因。

Edit2:Greg指出示例2将失败,因为该属性是只读属性,并且该__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)

显然,作为一门课,这几乎是毫无用处的,但它为您提供了继续学习的想法。


哦,虽然您在上课时不会安静下来,对吗?init self.test = 20中的文件“ Script1.py”,第5行,AttributeError:无法设置属性
Greg

真正。我将解决这个问题作为第三个示例。发现得好。
单身

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 __方法,最终退回到对象。__ getattribute __方法,如果其他祖先没有覆盖它。


5

如何__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编写应如下所示(可以使用setter和Deleter来复制明显的预期行为):

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__。这就是为什么您会得到递归错误的原因。您可以检查名称是否可用,"__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
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.