使用__getattr__并实现子类的预期行为
我是这个简单数据库层的作者,目前几乎可以肯定我是唯一的用户,虽然在多个项目中使用。这个数据库层是为MongoDB设计的,叫做 kale
,灵感来源于 minimongo。我在模型的基类中使用 __getattr__
,结果导致了一些难以追踪的错误。
我遇到的问题在去年六月由David Halter在这个网站上简洁地表达过。讨论很有趣,但没有提供解决方案。
简单来说:
>>> class A(object):
... @property
... def a(self):
... print "We're here -> attribute lookup found 'a' in one of the usual places!"
... raise AttributeError
... return "a"
...
... def __getattr__(self, name):
... print "We're here -> attribute lookup has not found the attribute in the usual places!"
... print('attr: ', name)
... return "not a"
...
>>> print(A().a)
We're here -> attribute lookup found 'a' in one of the usual places!
We're here -> attribute lookup has not found the attribute in the usual places!
('attr: ', 'a')
not a
>>>
需要注意的是,这种矛盾的行为并不是我从 官方Python文档中所期待的:
object.__getattr__(self, name)
当在通常的地方找不到属性时会被调用(也就是说,它既不是实例属性,也不在self的类树中)。name是属性的名称。
(如果他们提到 AttributeError
是“属性查找”用来判断属性是否在“通常地方”找到的方式,那就更好了。这个澄清的括号说明在我看来至少是不完整的。)
实际上,这导致了在 @property
描述符中抛出 AttributeError
时,追踪编程错误造成的bug变得困难。
>>> class MessedAttrMesser(object):
... things = {
... 'one': 0,
... 'two': 1,
... }
...
... def __getattr__(self, attr):
... try:
... return self.things[attr]
... except KeyError as e:
... raise AttributeError(e)
...
... @property
... def get_thing_three(self):
... return self.three
...
>>>
>>> blah = MessedAttrMesser()
>>> print(blah.one)
0
>>> print(blah.two)
1
>>> print(blah.get_thing_three)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 11, in __getattr__
AttributeError: 'get_thing_three'
>>>
在这种情况下,通过检查整个类,问题显而易见。然而,如果你依赖于堆栈跟踪中的消息 AttributeError: 'get_thing_three'
,那就没什么意义,因为显然 get_thing_three
看起来是一个有效的属性。
kale
的目的是提供一个基类来构建模型。因此,基模型代码对最终程序员是隐藏的,掩盖这种错误的原因并不是理想的。
最终程序员(咳咳 我)可能会选择在他们的模型上使用 @property
描述符,他们的代码应该以他们预期的方式工作和失败。
问题
我该如何让 AttributeError
在我定义了 __getattr__
的基类中传播?
3 个回答
我希望这里还能有更多的想法涌现出来。不过到现在为止,还没有符合我要求的!这可能有点难,但我至少离目标更近了一些:
>>> class GetChecker(dict):
... def __getattr__(self, attr):
... try:
... return self[attr]
... except KeyError as e:
... if hasattr(getattr(type(self), attr), '__get__'):
... raise AttributeError('ooh, this is an tricky error.')
... else:
... raise AttributeError(e)
...
... @property
... def get_thing_three(self):
... return self.three
...
>>>
>>> blah = GetChecker({'one': 0})
>>> print(blah.one)
0
>>> print(blah.lalala)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 6, in __getattr__
AttributeError: type object 'GetChecker' has no attribute 'lalala'
>>> print(blah.get_thing_three)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 7, in __getattr__
AttributeError: ooh, this is an tricky error.
>>>
至少这样我可以提供一个错误信息,能提示用户如何找到问题,而不是让人觉得问题就是这个...
不过我还不满意。我很乐意接受一个能做得更好的答案!
你的代码发生了什么:
首先来看类 A
的情况:
>>>print(A().a)
- 创建一个
A
的实例 - 访问这个实例上叫做
'a'
的属性
接下来,Python 会根据它的数据模型,尝试通过 object.__getattribute__
来查找 A.a
(因为你没有提供自定义的 __getattribute__
方法)
但是:
@property
def a(self):
print "We're here -> attribute lookup found 'a' in one of the usual places!"
raise AttributeError # <= an AttributeError is raised - now python resorts to '__getattr__'
return "a" # <= this code is unreachable
所以,由于 __getattribute__
查找结果是 AttributeError
,它会调用你的 __getattr__
方法:
def __getattr__(self, name):
... print "We're here -> attribute lookup has not found the attribute in the usual places!"
... print('attr: ', name)
... return "not a" #it returns 'not a'
接下来看看你的第二段代码:
你通过 __getattribute__
访问 blah.get_thing_three
。因为 get_thing_three
失败了(在 things
中没有 three
),所以会抛出一个 AttributeError,现在你的 __getattr__
尝试在 things
中查找 get_thing_three
,这也失败了 - 你会因为 get_thing_three
报错,因为它的优先级更高。
你可以做的事情:
你需要同时写自定义的 __getattribute__
和 __getattr__
。不过在大多数情况下,这样做并不会让你走得更远,其他使用你代码的人也不会期待有一些自定义的数据协议。
我有个建议给你(我写了一个粗糙的 MongoDB ORM,内部使用):不要在你的文档到对象的映射器中依赖 __getattr__
。在你的类中直接访问文档(我觉得这样不会破坏封装)。以下是我的示例:
class Model(object):
_document = { 'a' : 1, 'b' : 2 }
def __getattr__(self, name):
r"""syntactic sugar for those who are using this class externally.
>>>foo = Model()
>>>foo.a
1"""
@property
def ab_sum(self):
try :
return self._document[a] + self._document[b]
except KeyError:
raise #something that isn't AttributeError
简单来说,你是做不到的——或者说,至少没有简单且可靠的方法。正如你提到的,AttributeError
是 Python 用来判断某个属性是否“在通常的位置找到”的机制。虽然 __getattr__
的文档没有提到这一点,但在 __getattribute__
的文档中,这一点解释得更清楚,具体可以参考你已经链接的这个回答。
你可以重写 __getattribute__
并在其中捕获 AttributeError
,但是如果你捕获了一个错误,你就没有明显的方法来判断它是一个“真实”的错误(也就是说,属性确实没有找到),还是在查找过程中用户代码引发的错误。理论上,你可以检查错误的追踪记录,寻找特定的内容,或者做各种其他的技巧来验证属性是否存在,但这些方法会比现有的行为更脆弱和危险。
还有一种可能性是写一个基于 property
的自定义描述符,它可以捕获 AttributeErrors 并将其重新抛出为其他类型的错误。不过,这样的话,你就需要使用这个替代的属性,而不是内置的 property
。此外,这意味着从描述符方法内部引发的 AttributeErrors 不会作为 AttributeErrors 传播,而是作为其他类型的错误(你替换成的类型)。下面是一个例子:
class MyProp(property):
def __get__(self, obj, cls):
try:
return super(MyProp, self).__get__(obj, cls)
except AttributeError:
raise ValueError, "Property raised AttributeError"
class A(object):
@MyProp
def a(self):
print "We're here -> attribute lookup found 'a' in one of the usual places!"
raise AttributeError
return "a"
def __getattr__(self, name):
print "We're here -> attribute lookup has not found the attribute in the usual places!"
print('attr: ', name)
return "not a"
>>> A().a
We're here -> attribute lookup found 'a' in one of the usual places!
Traceback (most recent call last):
File "<pyshell#8>", line 1, in <module>
A().a
File "<pyshell#6>", line 6, in __get__
raise ValueError, "Property raised AttributeError"
ValueError: Property raised AttributeError
在这里,AttributeErrors 被替换成了 ValueErrors。如果你只是想确保这个异常“跳出”属性访问机制并能向上传播到下一个层级,这样做是可以的。但如果你有复杂的异常捕获代码,期待看到 AttributeError,它就会错过这个错误,因为异常类型已经改变了。
(另外,这个例子显然只处理了属性的获取,而不是设置,但扩展这个想法应该是很清楚的。)
我想作为这个解决方案的扩展,你可以将这个 MyProp
的想法与自定义的 __getattribute__
结合起来。基本上,你可以定义一个自定义异常类,比如 PropertyAttributeError
,并让属性替代在捕获到 AttributeError 时重新抛出为 PropertyAttributeError。然后,在你的自定义 __getattribute__
中,你可以捕获 PropertyAttributeError 并将其重新抛出为 AttributeError。基本上,MyProp
和 __getattribute__
可以作为一个“旁路”,绕过 Python 的正常处理,将错误从 AttributeError 转换为其他类型,然后在“安全”的时候再转换回 AttributeError。不过,我觉得这样做不太值得,因为 __getattribute__
可能会对性能产生显著影响。
还有一点补充:在 Python 的错误追踪器上,关于这个问题已经提出了一个 bug,最近也有关于可能解决方案的活动,所以未来的版本中可能会修复这个问题。