AttributeErrors: нежелательное взаимодействие между @property и __getattr__

У меня проблема с AttributeErrors, поднятым в @property в сочетании с __getattr__() в python:

Пример кода:

>>> 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() содержит ошибку.


Основываясь на идее в ответе Тадга Макдональда-Дженсена, мое лучшее решение на данный момент заключается в следующем. Буду очень признателен за любые подсказки о том, как избавиться от префикса __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'

person ARF    schedule 12.04.2016    source источник
comment
вам нужно было бы сделать __qualname__ = "AttributeError" в определении класса, чтобы удалить часть __main__, но поверьте мне, вы не хотите, чтобы ошибка просто говорила AttributeError, потому что тогда вы рискуете быть сбитым с толку, почему, черт возьми, except AttributeError не поймал ошибка атрибута.   -  person Tadhg McDonald-Jensen    schedule 13.04.2016


Ответы (2)


Вы можете создать пользовательское исключение, которое выглядит как 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.


Я совершенно уверен, что лучшее решение вашей проблемы уже было предложено, и вы можете увидеть предыдущая версия моего ответа, почему я считаю, что это лучший вариант. Хотя, честно говоря, если есть ошибка, которая неправильно подавляется, то поднимите чертову 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

Таким образом, когда что-то пойдет не так, чего вы не ожидаете, вы сразу узнаете об этом!

person Tadhg McDonald-Jensen    schedule 12.04.2016
comment
Это на самом деле не делает вещи намного лучше: трассировка стека все еще далека! Обратите внимание, что ошибка находится в функции deeply_nested_factory_fn, а не в функции inner, как предполагает трассировка стека, созданная вашим решением. - Тем не менее, мне бы очень понравилась ваша идея, позволяющая избежать sys.exit(1)..., если бы проблема с трассировкой стека могла быть исправлена. - person ARF; 13.04.2016
comment
После некоторого головокружения я придумал решение, которым я не слишком недоволен. Если вы все еще заинтересованы, посмотрите измененный вопрос. - person ARF; 13.04.2016
comment
Это помешает людям делать except AttributeError, чтобы поймать это исключение. - person user2357112 supports Monica; 13.04.2016
comment
@ user2357112 Это отличный момент. Мне придется еще немного подумать о вещах. - person ARF; 13.04.2016
comment
@ TadhgMcDonald-Jensen На самом деле, для моего приложения желательно поведение hasattr, которое вы иллюстрируете. У меня проблема, что hasattr молча терпит неудачу в блоке if hasattr(...): .... else: .... - не потому, что свойство не определено, а потому, что в реализации есть ошибка. Конечно, этот другой фрагмент кода должен вообще избегать hasattr и вместо этого просто использовать блок try: some_obj.attr except AttributeError: ....... - person ARF; 13.04.2016
comment
@TadhgMcDonald-Jensen Чтобы избежать этой проблемы в целом, было бы неплохо, если бы можно было повторно вызвать ошибку атрибута в __getattr__ в зависимости от того, возникло ли AttributeError в свойстве или нет. Но я думаю, как только мы доберемся до __getattr__, все следы AttributeError из собственности исчезнут. Или есть способ как-то восстановить эту ошибку и ее трассировку стека? - person ARF; 13.04.2016
comment
Я отредактировал свой ответ, чтобы предложить цепочку исключений, как я сделал с этот вопрос. И я скажу вам то же, что и ему: подделка бесшовного сообщения об ошибке — это не то, чем вы хотите заниматься. Вам нужна ошибка, которая сообщает вам, что на самом деле произошло во время выполнения, и будьте счастливы, что python выдает такие информативные сообщения трассировки. - person Tadhg McDonald-Jensen; 13.04.2016
comment
Спасибо за предложение цепочки RuntimeError. Это выглядит как простое, чистое решение для моей конкретной ситуации. - person ARF; 13.04.2016

Если вы хотите использовать исключительно классы нового стиля, вы можете перегрузить __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'
person Kevin    schedule 12.04.2016
comment
Спасибо, моя проблема с этим решением связана с падением производительности для других атрибутов. (Некоторые из них могут быть доступны в циклах.) - person ARF; 12.04.2016
comment
@ARF, если вас беспокоит производительность, получите локальные ссылки на часто используемые атрибуты. Я бы рекомендовал сделать это даже без небольшого замедления из-за перегрузки __getattribute__. - person Tadhg McDonald-Jensen; 12.04.2016
comment
Вы имеете в виду a = test.a, а затем используете a вместо test.a? - person ARF; 12.04.2016
comment
@ARF да, я действительно рекламировал ответ Кевина в своем, потому что он намного превосходит мой. - person Tadhg McDonald-Jensen; 13.04.2016