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__
. Любые мысли?
Ответы
Ответ 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
.
Вы также можете зарегистрировать его, если хотите.