2016-04-12 1 views
2

J'ai un problème avec AttributeErrors élevé dans une @property en combinaison avec __getattr__() en python:AttributeErrors: interaction indésirable entre @property et __getattr__

Exemple Code:

>>> 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' 

Dans mon cas, c'est un message d'erreur très trompeur, car il cache le fait que deeply_nested_factory_fn() a une erreur. Sur la base de l'idée contenue dans la réponse de Tadhg McDonald-Jensen, ma meilleure solution actuelle est la suivante. Tous les conseils sur la façon de se débarrasser du préfixe __main__. à AttributeError et la référence à attributeErrorCatcher dans le retraçage serait très appréciée.

>>> 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' 
+0

vous devez faire '__qualname__ =" AttributeError "' dans la définition de la classe pour supprimer la partie '__main__' mais croyez-moi, vous ne ** voulez pas que l'erreur indique simplement" AttributeError "car vous risquez d'être dérouté pourquoi le diable 'except AttributeError' n'a pas attrapé une AttributeError. –

Répondre

1

Vous pouvez créer une exception personnalisée qui semble être un AttributeError mais ne déclenchera pas __getattr__ car il est pas vraiment un AttributeError.

MISE À JOUR: le message de retraçage est grandement améliorée en réaffectant l'attribut .__traceback__ avant-relançant l'erreur:

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 

Ensuite, lorsque vous définissez votre propriété:

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

et le message d'erreur vous obtiendrez ressemblera à ceci:

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' 

remarquez le re est une ligne pour raise new_err from None mais il est au-dessus des lignes de l'appel de la propriété. Il y aurait également une ligne pour return f(*args,**kw) mais qui est omise avec .tb_next.


Je suis assez sûr que la meilleure solution à votre problème a already been suggested et vous pouvez voir le previous revision de ma réponse pourquoi je pense qu'il est la meilleure option.Bien honnêtement s'il y a une erreur qui est mal réprimée alors élever une RuntimeError sanglante enchaîné à celui qui serait caché autrement:

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 

alors si vous décorez votre propriété avec cela, vous obtiendrez une erreur comme ceci:

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 

Bien que si vous attendez plus que juste une chose à soulever une AttributeError alors vous pourriez vouloir simplement surcharger __getattribute__ pour vérifier toute erreur particulière pour tous: lookups

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 

De cette façon, quand quelque chose va mal que vous ne vous attendez pas, vous le saurez tout de suite!

+0

Cela ne va pas vraiment mieux: la trace de la pile est encore loin! Notez que l'erreur est dans la fonction 'deep_nested_factory_fn' pas dans la fonction' inner' comme suggéré par la trace de la pile produite par votre solution. - Cela dit, j'apprécierais vraiment votre idée qui évite 'sys.exit (1)' ... si le problème de stacktrace pouvait être résolu. – ARF

+0

Après quelques égratignures, je suis venu avec une solution dont je ne suis pas trop mécontent. Si vous êtes toujours intéressé, jetez un oeil à la question modifiée. – ARF

+0

Cela va empêcher les gens de faire 'exception except AttributeError' pour attraper cette exception. – user2357112

2

Si vous êtes prêt à utiliser exclusivement des cours nouveau style, vous pouvez surcharger __getattribute__ au lieu de __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() 

Maintenant, votre trace de la pile mentionnera correctement 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' 
+0

Merci, mon problème avec cette solution est le coup de performance pour les autres attributs. (Certains d'entre eux peuvent être consultés dans les boucles.) – ARF

+0

@ARF, si les performances sont un problème, obtenez des références locales aux attributs fréquemment utilisés. Je recommande de faire cela même sans _ ralentir légèrement en surchargeant '__getattribute__'. –

+0

Vous voulez dire par 'a = test.a' et ensuite en utilisant' a' au lieu de 'test.a'? – ARF