当__getattr__()遇上@property,坑倒Python老司机

今天同事反馈说我写的一个基础库有一个bug,大概就是自己写的类明明有属性foo,但会抛个类似下边的异常出来,

AttributeError: 'A' object has no attribute 'foo'

这很让人困惑啊,因为抛出异常的函数是基类的__getattr__()方法,所以他就找我来解决了。

我看代码也是一脸懵,这个foo就摆在那里,这个bug给了我一个眼见不为实的错觉,一时找不到方向。突然我发现这个foo上面顶着个@property的帽子(装饰器),咦,会不会和这个有关系呢?于是搜索一下,就找到了这篇文章Correct handling of AttributeError in __getattr__ when using property,上面的高赞回答完美解答了这个问题。下面我就以这个问答的代码为例再讲一下,这样大家可以只看我这篇了。

首先有这样的代码,这个代码纯作示例用的,没有任何逻辑意义,

class A:

    @property
    def F(self):
        return self.moo # here should be an error

    @property
    def G(self):
        return self.F

    def __getattr__(self, name):
        print('call of __getattr__ with name =', name)
        if name == 'foo':
            return 0
        raise AttributeError("'{}' object has no attribute '{}'".format(type(self).__name__, name))

a = A()
print(a.G)

显然地,会以为抛出的异常是AttributeError: 'A' object has no attribute 'moo',但实际抛出的异常却是AttributeError: 'A' object has no attribute 'G'而且吐出来的堆栈是这样的:

Traceback (most recent call last):
  line 18 in <module>
    print(a.G)
  line 15, in __getattr__
    raise AttributeError("'{}' object has no attribute '{}'".format(type(self).__name__, name))
AttributeError: 'A' object has no attribute 'G'

作为__getattr__()的作者,简直能气死个人。

消消气,那么为什么__getattr__()会把前面AtrributeError异常吃掉了呢?这就要先回顾一下__getatrr__()的调用时机,文档中说:

Called when the default attribute access fails with an AttributeError (either __getattribute__() raises an AttributeError because name is not an instance attribute or an attribute in the class tree for self; or __get__() of a name property raises AttributeError).

简单来说,当查找属性时,Python会先调用__getattribute__(),如果找不到(抛出AtrributeError异常),才会去尝试调用__getattr__()。还有一种情况就是如果property__get__()方法抛出AttributeError异常时,也会尝试调用__getattr__()

那么回到上例,使用@property修饰的成员函数就成一个描述器descriptor,当访问它的时候,实际调用的是描述器的__get__()方法,显然,print(a.G)先调用了a.G.__get__,然后它又调用了a.F.__get__,而这个方法引用了self.moo,因为moo属性不存在,就抛出了AtrributeError。根据上面文档的描述,此时会调用a__getattr__()方法,根据上面的代码,这个__getattr__()也会抛出AtrributeError,所以Python就把前一个异常吃掉了,只显示了后面那个牛头不对马嘴的异常。

如此说来,那就是要在__get__()抛出AtrributeError的时候接住异常,而不是由Python去处理。要实现这一点,在这个例子和我们项目的代码里,都是重载__getatrribute__()而不是__getattr__()。把上面的代码修改如下:

class A:

    @property
    def F(self):
        return self.moo # here should be an error

    @property
    def G(self):
        return self.F

    def __getattribute__(self, name):
        print('call of __getattribute__ with name =', name)
        if name == 'foo':
            return 0
        else:
            return super().__getattribute__(name)

再次执行的时候,就会发现抛出来的异常符合期望了:

Traceback (most recent call last):
...
AttributeError: 'A' object has no attribute 'moo'

而我们项目做了同样的修改之后,也解决了问题。这个事故告诉我们,轻易不要去重载__getattr__()

上一篇:AttributeError: ‘Line2D‘ object has no property ‘lable‘


下一篇:pymysql常见报错