Подтвердить что ты не робот

Python: Почему __getattr__ ловит AttributeErrors?

Я борюсь с __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__ documentation). getattr() может просто использовать A.__dict__. Любые мысли?

4b9b3361

Ответ 1

Я просто изменил код на

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__() называется "последним средством".

Он не документирован четко, но может быть подсчитан в разделе "Вызывается, когда поиск атрибута не нашел атрибут в обычных местах".

Ответ 2

__getattribute__ документация говорит:

Если класс также определяет __getattr__(), последний не будет вызываться, если только __getattribute__() не вызывает его явно или не создает AttributeError.

Я прочитал (с помощью inclusio unius est exclusio alterius), говоря, что доступ к атрибуту будет вызывать __getattr__, если object.__getattribute__ (который называется "безоговорочно для реализации доступа к атрибутам" ) AttributeError - непосредственно или внутри дескриптора __get__ (например, свойство fget); обратите внимание, что __get__ должно "вернуть значение (вычисленное) значение атрибута или повысить исключение AttributeError".

Как аналогия, специальные методы оператора могут поднять NotImplementedError, после чего будут проверены другие методы оператора (например, __radd__ для __add__).

Ответ 3

Использование __getattr__ и свойств в одном классе опасно, потому что это может привести к ошибкам, которые очень трудно отлаживать.

Если получатель свойства выбрасывает AttributeError, тогда AttributeError бесшумно вылавливается и вызывается __getattr__. Обычно это приводит к ошибке __getattr__ с исключением, но если вам очень не повезло, это не так, и вы даже не сможете легко отследить проблему до __getattr__.

Если ваш имущественный геттер тривиален, вы никогда не сможете быть на 100% уверены, что он не бросит AttributeError. Исключение может быть сброшено на несколько уровней.

Вот что вы могли бы сделать:

  • Избегайте использования свойств и __getattr__ в том же классе.
  • Добавьте блок try ... except ко всем свойствам, которые не являются тривиальными
  • Сохраняйте свойство getters простым, поэтому вы знаете, что они не будут бросать AttributeError
  • Напишите свою собственную версию декодера @property, которая ловит AttributeError и перебрасывает ее как RuntimeError.

См. также http://blog.devork.be/2011/06/using-getattr-and-property_17.html

EDIT: если кто-то рассматривает решение 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__.

Ответ 4

__getattr__ вызывается, когда доступ к атрибуту завершается с помощью атрибута AttributeError. Возможно, именно поэтому вы думаете, что это "ловушки" ошибок. Однако, это не так, это функция доступа к атрибуту Python, которая их ловит, а затем вызывает __getattr__.

Но __getattr__ сам не поймает никаких ошибок. Если вы поднимаете AttributeError в __getattr__, вы получаете бесконечную рекурсию.

Ответ 5

В любом случае вы обречены, когда вы объединяете @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__ определено), но все равно будет достигнут атрибут "жизнь", поскольку он не существует, тогда как реальная основная проблема заключается в Paradise.breasts.

Мое частичное решение включает использование блоков try, кроме @property, которые, как известно, попадают в исключения AttributeError.

Ответ 6

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