AttributeErrors:@property和__getattr__之间不希望的交互

时间:2016-04-12 13:39:31

标签: python properties attributeerror getattr

我在AttributeErrors中与@property结合使用__getattr__()时遇到问题:

示例代码:

>>> def deeply_nested_factory_fn():
...     a = 2
...     return a.invalid_attr
...
>>> class Test(object):
...     def __getattr__(self, name):
...         if name == 'abc':
...             return 'abc'
...         raise AttributeError("'Test' object has no attribute '%s'" % name)
...     @property
...     def my_prop(self):
...         return deeply_nested_factory_fn()
...
>>> test = Test()
>>> test.my_prop
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 5, in __getattr__
AttributeError: 'Test' object has no attribute 'my_prop'

就我而言,这是一个极具误导性的错误消息,因为它隐藏了deeply_nested_factory_fn()出错的事实。

根据Tadhg McDonald-Jensen的回答,我目前最好的解决方案如下。关于如何摆脱__main__. AttributeError前缀以及追溯中attributeErrorCatcher的引用的任何提示都将非常感激。

>>> def catchAttributeErrors(func):
...     AttributeError_org = AttributeError
...     def attributeErrorCatcher(*args, **kwargs):
...         try:
...             return func(*args, **kwargs)
...         except AttributeError_org as e:
...             import sys
...             class AttributeError(Exception):
...                 pass
...             etype, value, tb = sys.exc_info()
...             raise AttributeError(e).with_traceback(tb.tb_next) from None
...     return attributeErrorCatcher
...
>>> def deeply_nested_factory_fn():
...     a = 2
...     return a.invalid_attr
...
>>> class Test(object):
...     def __getattr__(self, name):
...         if name == 'abc':
...             # computing come other attributes
...             return 'abc'
...         raise AttributeError("'Test' object has no attribute '%s'" % name)
...     @property
...     @catchAttributeErrors
...     def my_prop(self):
...         return deeply_nested_factory_fn()
...
>>> class Test1(object):
...     def __init__(self):
...         test = Test()
...         test.my_prop
...
>>> test1 = Test1()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 4, in __init__
  File "<stdin>", line 11, in attributeErrorCatcher
  File "<stdin>", line 10, in my_prop
  File "<stdin>", line 3, in deeply_nested_factory_fn
__main__.AttributeError: 'int' object has no attribute 'invalid_attr'

2 个答案:

答案 0 :(得分:2)

如果您愿意专门使用新式课程,可以重载__getattribute__而不是__getattr__

class Test(object):
    def __getattribute__(self, name):
        if name == 'abc':
            return 'abc'
        else:
            return object.__getattribute__(self, name)
    @property
    def my_prop(self):
        return deeply_nested_factory_fn()

现在您的堆栈跟踪将正确提及deeply_nested_factory_fn

Traceback (most recent call last):
  File "C:\python\myprogram.py", line 16, in <module>
    test.my_prop
  File "C:\python\myprogram.py", line 10, in __getattribute__
    return object.__getattribute__(self, name)
  File "C:\python\myprogram.py", line 13, in my_prop
    return deeply_nested_factory_fn()
  File "C:\python\myprogram.py", line 3, in deeply_nested_factory_fn
    return a.invalid_attr
AttributeError: 'int' object has no attribute 'invalid_attr'

答案 1 :(得分:1)

您可以创建一个看似AttributeError的自定义异常,但不会触发__getattr__,因为它实际上不是AttributeError

更新:通过在重新引发错误之前重新分配.__traceback__属性,大大改善了回溯消息:

class AttributeError_alt(Exception):
    @classmethod
    def wrapper(err_type, f):
        """wraps a function to reraise an AttributeError as the alternate type"""
        @functools.wraps(f)
        def alt_AttrError_wrapper(*args,**kw):
            try:
                return f(*args,**kw)
            except AttributeError as e:
                new_err = err_type(e)
                new_err.__traceback__ = e.__traceback__.tb_next
                raise new_err from None
        return alt_AttrError_wrapper

然后将属性定义为:

@property
@AttributeError_alt.wrapper
def my_prop(self):
    return deeply_nested_factory_fn()

,您将收到的错误消息如下所示:

Traceback (most recent call last):
  File ".../test.py", line 34, in <module>
    test.my_prop
  File ".../test.py", line 14, in alt_AttrError_wrapper
    raise new_err from None
  File ".../test.py", line 30, in my_prop
    return deeply_nested_factory_fn()
  File ".../test.py", line 20, in deeply_nested_factory_fn
    return a.invalid_attr
AttributeError_alt: 'int' object has no attribute 'invalid_attr'

注意 raise new_err from None的一行,但它位于属性调用中的行之上。 return f(*args,**kw)也会有一行,但.tb_next会省略。

我相当确定问题的最佳解决方案有already been suggested,您可以看到我的答案的previous revision,为什么我认为这是最佳选择。虽然老实说如果有一个错误被错误地压制,那么就将一个血腥的RuntimeError链接到一个隐藏的那个:

def assert_no_AttributeError(f):
    @functools.wraps(f)
    def assert_no_AttrError_wrapper(*args,**kw):
        try:
            return f(*args,**kw)
        except AttributeError as e:
            e.__traceback__ = e.__traceback__.tb_next
            raise RuntimeError("AttributeError was incorrectly raised") from e
    return assert_no_AttrError_wrapper

然后如果你用这个装饰你的财产,你会得到这样的错误:

Traceback (most recent call last):
  File ".../test.py", line 27, in my_prop
    return deeply_nested_factory_fn()
  File ".../test.py", line 17, in deeply_nested_factory_fn
    return a.invalid_attr
AttributeError: 'int' object has no attribute 'invalid_attr'

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

Traceback (most recent call last):
  File ".../test.py", line 32, in <module>
    x.my_prop
  File ".../test.py", line 11, in assert_no_AttrError_wrapper
    raise RuntimeError("AttributeError was incorrectly raised") from e
RuntimeError: AttributeError was incorrectly raised

虽然如果你期望引发一个AttributeError只有一件事,那么你可能只想重载__getattribute__以检查所有查找的任何特殊错误:

def __getattribute__(self,attr):
    try:
        return object.__getattribute__(self,attr)
    except AttributeError as e:
        if str(e) == "{0.__class__.__name__!r} object has no attribute {1!r}".format(self,attr):
            raise #normal case of "attribute not found"
        else: #if the error message was anything else then it *causes* a RuntimeError
            raise RuntimeError("Unexpected AttributeError") from e

这种方式出现问题时,你没想到你会立刻知道它!