使用__getattr__并实现子类的预期行为

5 投票
3 回答
2733 浏览
提问于 2025-04-17 19:02

我是这个简单数据库层的作者,目前几乎可以肯定我是唯一的用户,虽然在多个项目中使用。这个数据库层是为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 个回答

0

我希望这里还能有更多的想法涌现出来。不过到现在为止,还没有符合我要求的!这可能有点难,但我至少离目标更近了一些:

>>> 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.
>>> 

至少这样我可以提供一个错误信息,能提示用户如何找到问题,而不是让人觉得问题就是这个...

不过我还不满意。我很乐意接受一个能做得更好的答案!

2

你的代码发生了什么:

首先来看类 A 的情况:

>>>print(A().a)

  1. 创建一个 A 的实例
  2. 访问这个实例上叫做 '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
6

简单来说,你是做不到的——或者说,至少没有简单且可靠的方法。正如你提到的,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,最近也有关于可能解决方案的活动,所以未来的版本中可能会修复这个问题。

撰写回答