今天同事反馈说我写的一个基础库有一个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__()
。