使用__getattr__并满足子类的预期行为

时间:2013-03-14 04:24:20

标签: python

我是作者 - 目前几乎绝对是唯一的用户,如果在多个项目中,a simple database layer(受minimongo启发)的MongoDB称为kale。我目前在模型基类中使用__getattr__导致了一些难以跟踪的错误。

我在去年六月由David Halter在本网站上简明扼要地遇到了问题was articulated。讨论很有意思,但没有提供解决方案。

简而言之:

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

请注意,这种矛盾的行为并不是我对阅读the official python documentation所期望的:

  

object.__getattr__(self, name)

     

当属性查找未找到属性时调用   通常的地方(即它不是一个实例属性,也没有找到   自我的类树。 name是属性名称。

(如果他们提到AttributeError是“属性查找”知道在“通常的地方”中是否找到某个属性的方法,那将是很好的。澄清括号在我看来最好不完整的。)

实际上,这导致了在@property描述符事件中引发AttributeError的情况下跟踪编程错误导致的错误的问题。

>>> 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的目的是为构建模型提供基类。因此,基本模型代码远离最终程序员隐藏,并且掩盖这样的错误原因并不理想。

最终程序员( cough me)可能会选择在他们的模型上使用@property描述符,并且他们的代码应该以他们的方式运行失败期望的。

问题

如何允许AttributeError通过已定义__getattr__的基类传播?

3 个答案:

答案 0 :(得分:5)

基本上,你不能 - 或者至少,不能以简单和万无一失的方式。如您所述,AttributeError是Python用于确定属性是否“在通常位置找到”的机制。虽然__getattr__文档没有提到这一点,但在__getattribute__的文档中更清楚一些,如this answer中对已经链接到的问题所述。

你可以覆盖__getattribute__并在那里捕捉AttributeError,但是如果你抓到了property,你就没有明显的方法来判断它是否是一个“真正的”错误(意味着属性真的不是'发现),或在查找过程中由用户代码引发的错误。从理论上讲,你可以检查寻找特定事物的追溯,或者做各种其他黑客来试图验证属性的存在,但这些方法将比现有的行为更加脆弱和危险。

另一种可能性是编写自己的基于property的描述符,但捕获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不会作为AttributeErrors传播,而是作为其他东西传播(无论你用它们替​​换它们)。这是一个例子:

MyProp

此处AttributeErrors将替换为ValueErrors。如果你想要的只是确保异常“突破”属性访问机制并且可以传播到下一级别,这可能没问题。但是,如果您有复杂的异常捕获代码,期望看到一个AttributeError,它将错过此错误,因为异常类型已更改。

(另外,这个例子显然只涉及属性获取者,而不是setter,但应该清楚如何扩展这个想法。)

我想作为此解决方案的扩展,您可以将此__getattribute__提示与自定义PropertyAttributeError结合使用。基本上,您可以定义一个自定义异常类,比如__getattribute__,并将属性替换重新引用AttributeError作为PropertyAttributeError。然后,在您的自定义MyProp中,您可以捕获PropertyAttributeError并将其重新引发为AttributeError。基本上__getattribute____getattribute__可以充当“分流器”,通过将错误从AttributeError转换为其他东西来绕过Python的正常处理,然后一旦“安全”到它就再次将其转换回AttributeError。这样做。但是,我觉得这样做是不值得的,因为{{1}}会对性能产生重大影响。

一个小小的附录:a bug已经在Python错误跟踪器上提出了这个问题,并且最近有关于可能解决方案的活动,因此可能会在将来的版本中修复它。

答案 1 :(得分:2)

您的代码中会发生什么:

A课的第一个案例:

>>>print(A().a)

  1. 创建A
  2. 的实例
  3. 在实例
  4. 上访问名为'A'的属性

    现在python,按照其数据模型,尝试使用A.a找到object.__getattribute__(因为您还没有提供自定义__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'
    

    第二段代码中会发生什么:

    您可以blah.get_thing_three访问__getattribute__。由于get_thing_three因属性错误而失败(three中没有things),现在您的__getattr__尝试在get_thing_three中查找things,这也会失败 - 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
    

答案 2 :(得分: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.
>>> 

至少我可以提供一条错误消息,暗示如何追查问题,而不是看起来像问题...

但是,我不满意。我很乐意接受一个可以做得更好的答案!