Python:为什么 __getattr__ 会捕获 AttributeErrors?

问题描述 投票:0回答:7

我正在与

__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__
。有什么想法吗?

python exception python-3.x python-2.7
7个回答
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__()
被称为“最后的手段”。

它没有明确记录,但可以算在“当属性查找在通常的地方没有找到属性时调用”。


9
投票

在同一个类中使用

__getattr__
和属性是危险的,因为它可能会导致非常难以调试的错误。

如果属性的 getter 抛出

AttributeError
,则
AttributeError
会被默默捕获,并调用
__getattr__
。通常,这会导致
__getattr__
失败并出现异常,但如果您非常不幸,则不会,而且您甚至无法轻松地将问题追溯到
__getattr__

编辑:此问题的示例代码可以在这个答案中找到。

除非你的属性获取器很简单,否则你永远不能 100% 确定它不会抛出

AttributeError
。异常可能会抛出几个深度级别。

您可以这样做:

  1. 避免在同一个类中使用属性和
    __getattr__
  2. 向所有重要的属性 getter 添加一个
    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)

然后在覆盖

@property_
的类中使用
@property
而不是
__getattr__


6
投票

__getattribute__
文档说:

如果该类还定义了

__getattr__()
,则不会调用后者,除非
__getattribute__()
显式调用它或引发
AttributeError

我读到这篇文章(由 inclusio unius est exclusio alterius)说属性访问 will 调用

__getattr__
如果
object.__getattribute__
(即“无条件调用来实现属性访问”)碰巧引发
AttributeError
- 无论是直接还是在描述符中
__get__
(例如属性 fget);请注意,
__get__
应该“返回(计算的)属性值或引发
AttributeError
异常
”。

打个比方,运算符特殊方法可以引发

NotImplementedError
,然后将尝试其他运算符方法(例如
__radd__
表示
__add__
)。


4
投票
当属性访问失败并出现 AttributeError 时,会调用

__getattr__
。也许这就是您认为它“捕获”错误的原因。然而,事实并非如此,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
的类的名称。 如果您愿意,也可以记录它。


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

我的部分解决方案涉及在

@property
块中使用 try-except,已知这些块会遇到
AttributeError
异常。


0
投票

我知道自从提出这个问题以来已经有一段时间了,但这是我的解决方法:

class PropertyAttributeError(Exception):
    pass


def getattr_safe_property(func):
    """The A class uses 'property', and overloads
    :obj:`__getattr__`. This is not a good idea, because if an AttributeError is
    raised during the evaluation of a property, then the error message will
    look like the property itself is not found.

    To avoid those misleading errors, all properties of A must be written as such:


    .. code-block:: python

        @property
        @getattr_safe_property
        def some_property(self):
            ...

    Since the new :obj:`~PropertyAttributeError` is raised from the original
    :obj:`AttributeError`, the error stack will contain the actual problematic
    line, but it will not be silently caught by :obj:`__getattr__`.
    """
    def wrapper(self):
        try:
            return func(self)
        except AttributeError as error:
            raise PropertyAttributeError(
                "An AttributeError was raised while evaluating the property "
                f"'{func.__name__}' of a {self.__class__.__name__} instance:"
                f" {error}"
            ) from error
    return wrapper


class A:

    def __init__(self):
        self._a = 0

    @property
    @getattr_safe_property
    def some_property(self):
        return {}

    def __getattr__(self, item):
        # Do some stuff
        raise AttributeError(f"No attribute {item}")

    @property
    @getattr_safe_property
    def a(self):
        print(self.some_property.wrong_attribute)
        return self._a

    @a.setter
    def a(self, value):
        self._a = value


b = A()
b.a = 3
print(b.a)

然后您将得到正确的错误堆栈:

Traceback (most recent call last):
  File "path/to/file.py", line 8, in wrapper
    return func(self)
           ^^^^^^^^^^
  File "path/to/file.py", line 34, in a
    print(self.some_property.wrong_attribute)
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
AttributeError: 'dict' object has no attribute 'wrong_attribute'

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "path/to/file.py", line 44, in <module>
    print(b.a)
          ^^^
  File "/path/to/file.py", line 10, in wrapper
    raise PropertyAttributeError(
PropertyAttributeError: An AttributeError was raised while evaluating the property 'a' of a A instance: 'dict' object has no attribute 'wrong_attribute'
© www.soinside.com 2019 - 2024. All rights reserved.