Python:为什么__getattr__会捕获AttributeError?

16

我在处理 __getattr__ 时遇到了困难。我的代码库是一个复杂的递归结构,让异常传播非常重要。

class A(object):
    @property
    def a(self):
        raise AttributeError('lala')

    def __getattr__(self, name):     
        print('attr: ', name)
        return 1      

print(A().a)

结果为:

('attr: ', 'a')
1

为什么会出现这种行为?为什么没有抛出异常?这种行为没有被记录在文档中(__getattr__文档)。 getattr() 可以直接使用 A.__dict__。有什么想法吗?

2
我认为文档的失败在于它声明 AttributeError 可能会在 __get__ 中被引发(这实际上是你在这里所做的),但没有解释其影响。文档应该明确说明 AttributeError 表示 Python 应该表现得好像未找到该属性一样(因此将尝试使用备用的 __getattr__)- 如果这不是您想要的行为,则应引发其他异常。 - James
6个回答

9
我刚刚修改了代码为:
class A(object):
    @property
    def a(self):
        print "trying property..."
        raise AttributeError('lala')
    def __getattr__(self, name):     
        print('attr: ', name)
        return 1      

print(A().a)

我们可以看到,实际上首先尝试的是属性。但由于它声称不存在(通过引发AttributeError),因此__getattr__()被称为“最后的手段”。

这并没有被清楚地记录下来,但可能被归类为“在通常的位置中未找到属性时调用”。


而推荐的做法是什么呢?不使用属性或者使用__getattribute__代替__getattr__吗? - Dave Halter
2
我认为不要引发AttributeError,或在__getattr__中处理该情况,或使用__getattribute__并调用super(...).__getattribute__,捕获AttributeError - glglgl
1
我真想不透为何你会有意在一个属性中显式引发AttributeError,或者在任何除了__(get|set)attr[ibute]__方法以外的地方引发。 - Karl Knechtel
明确地引发AttributeError并不是这个问题的关键...问题在于,我有一个复杂的程序,在其中由于编程错误而引发AttributeError,但我不知道它在哪里,因为__getattr__将其隐藏了。 - Dave Halter
3
并不是我主动引发了AttributeError,而只是犯了编程错误(访问了一个不存在的属性)。最终我什么也看不到,因为被__getattr__捕获了。 - Dave Halter
根据发生的情况,__getattr__ 处理此操作。如果您知道我们所讨论的属性,您可以在 __getattr__ 中引发异常(也许是断言?),或者仅在调试时引发异常,以便找到无效属性访问发生的位置。 - glglgl

9

在同一个类中同时使用__getattr__和属性是危险的,因为它可能导致非常难以调试的错误。

如果属性的getter引发AttributeError,那么AttributeError会被静默地捕获,并调用__getattr__。通常,这会导致__getattr__失败并引发异常,但如果你非常不幸,它不会,而且你甚至无法轻松追踪问题到__getattr__

编辑:此问题的示例代码可在this answer中找到。

除非您的属性getter是微不足道的,否则您永远无法100%确定它不会引发AttributeError。异常可能会被抛出几个级别。

以下是您可以做的:

  1. 避免在同一个类中使用属性和__getattr__
  2. 对于不是微不足道的属性获取器,添加try ... except块。
  3. 保持属性获取器简单,这样您就知道它们不会抛出AttributeError
  4. 编写自己版本的@property装饰器,它捕获AttributeError并将其重新抛出为RuntimeError

另请参见http://blog.devork.be/2011/06/using-getattr-and-property_17.html

编辑:如果有人考虑解决方案4(我不建议这样做),可以像这样完成:

def property_(f):
    def getter(*args, **kwargs):
        try:
            return f(*args, **kwargs)
        except AttributeError as e:
            raise RuntimeError, "Wrapped AttributeError: " + str(e), sys.exc_info()[2]

    return property(getter)

在覆盖 __getattr__ 的类中,使用 @property_ 代替 @property

6

__getattribute__文档说:

如果类也定义了__getattr__(),则只有在__getattribute__()明确调用它或引发AttributeError时,后者才会被调用。

我根据 inclusio unius est exclusio alterius 的原则理解为,如果object.__getattribute__("无条件地调用以实现属性访问")因任何原因抛出AttributeError,不管是直接抛出还是在描述符__get__(例如属性fget)内部抛出,那么属性访问将会调用__getattr__;需要注意的是,__get__应该 "返回(计算的)属性值或引发AttributeError异常"。

类比于操作符特殊方法可以抛出NotImplementedError,这时就会尝试其他操作符方法(例如__add____radd__)。


4

__getattr__方法会在属性访问发生AttributeError异常时被调用。也许这就是你认为它“捕获”错误的原因。然而,实际上是Python的属性访问功能捕获了这些错误,并随后调用__getattr__方法。

但是,__getattr__方法本身并不会捕获任何错误。如果你在__getattr__中引发AttributeError异常,将会导致无限递归。


1

我经常遇到这个问题,因为我经常实现__getattr__和许多@property方法。下面是一个装饰器,可以提供更有用的错误消息:

def replace_attribute_error_with_runtime_error(f):
    @functools.wraps(f)
    def wrapped(*args, **kwargs):
        try:
            return f(*args, **kwargs)
        except AttributeError as e:
            # logging.exception(e)
            raise RuntimeError(
                '{} failed with an AttributeError: {}'.format(f.__name__, e)
            )
    return wrapped

并像这样使用它:
class C(object):

    def __getattr__(self, name):
        ...

    @property
    @replace_attribute_error_with_runtime_error
    def complicated_property(self):
        ...

    ...

底层异常的错误信息将包括引发底层AttributeError的实例所属类的名称。如果您愿意,还可以记录它。

1
我曾经做过类似的工作,并称其为“safe_property”。它完成了相同的事情,只需要一个装饰器。 - Dave Halter
如果有人想使用单装饰器方法,我在https://github.com/davidhalter/jedi/blob/master/jedi/inference/utils.py找到了Dave Halter的代码。 - Dave Brondsema

0
无论如何,当您将@property与__getattr__结合使用时,您都注定会失败:
class Paradise:
    pass

class Earth:
    @property
    def life(self):
        print('Checking for paradise (just for fun)')
        return Paradise.breasts
    def __getattr__(self, item):
        print("sorry! {} does not exist in Earth".format(item))

earth = Earth()
try:
    print('Life in earth: ' + str(earth.life))
except AttributeError as e:
    print('Exception found!: ' + str(e))

输出以下内容:

Checking for paradise (just for fun)
sorry! life does not exist in Earth
Life in earth: None

当你真正的问题在于调用Paradise.breasts时。

__getattr__总是在引发AtributeError时被调用。异常的内容被忽略。

可悲的是,这个问题没有解决方案,因为hasattr(earth, 'life')将返回True(仅仅因为定义了__getattr__),但是仍然会被属性“life”所访问,因为它不存在,而真正的根本问题在于Paradise.breasts

我的部分解决方案涉及在已知会遇到AttributeError异常的@property块中使用try-except。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接