Ajouter des informations à une exception?

144

Je veux réaliser quelque chose comme ceci:

def foo():
   try:
       raise IOError('Stuff ')
   except:
       raise

def bar(arg1):
    try:
       foo()
    except Exception as e:
       e.message = e.message + 'happens at %s' % arg1
       raise

bar('arg1')
Traceback...
  IOError('Stuff Happens at arg1')

Mais ce que j'obtiens c'est:

Traceback..
  IOError('Stuff')

Des indices sur la façon d'y parvenir? Comment faire à la fois en Python 2 et 3?

Anijhaw
la source
En cherchant de la documentation pour l' messageattribut Exception, j'ai trouvé cette question SO, BaseException.message obsolète dans Python 2.6 , ce qui semble indiquer que son utilisation est maintenant déconseillée (et pourquoi elle n'est pas dans la documentation).
martineau
malheureusement, ce lien ne semble plus fonctionner.
Michael Scott Cuthbert
1
@MichaelScottCuthbert voici une bonne alternative: itmaybeahack.com/book/python-2.6/html/p02/…
Niels Keurentjes
Voici une très bonne explication de l'état de l'attribut de message et de sa relation avec l'attribut args et PEP 352 . Il est tiré du livre gratuit Building Skills in Python de Steven F. Lott.
martineau

Réponses:

119

Je le ferais comme ça, donc changer son type foo()ne nécessiterait pas de le changer également bar().

def foo():
    try:
        raise IOError('Stuff')
    except:
        raise

def bar(arg1):
    try:
        foo()
    except Exception as e:
        raise type(e)(e.message + ' happens at %s' % arg1)

bar('arg1')

Traceback (most recent call last):
  File "test.py", line 13, in <module>
    bar('arg1')
  File "test.py", line 11, in bar
    raise type(e)(e.message + ' happens at %s' % arg1)
IOError: Stuff happens at arg1

Mise à jour 1

Voici une légère modification qui préserve le retraçage d'origine:

...
def bar(arg1):
    try:
        foo()
    except Exception as e:
        import sys
        raise type(e), type(e)(e.message +
                               ' happens at %s' % arg1), sys.exc_info()[2]

bar('arg1')

Traceback (most recent call last):
  File "test.py", line 16, in <module>
    bar('arg1')
  File "test.py", line 11, in bar
    foo()
  File "test.py", line 5, in foo
    raise IOError('Stuff')
IOError: Stuff happens at arg1

Mise à jour 2

Pour Python 3.x, le code de ma première mise à jour est syntaxiquement incorrect et l'idée d'avoir un messageattribut sur a BaseExceptionété retirée lors d'un changement de PEP 352 le 16/05/2012 (ma première mise à jour a été publiée le 12/03/2012) . Donc actuellement, dans Python 3.5.2 de toute façon, vous devez faire quelque chose dans ce sens pour préserver le traçage et ne pas coder en dur le type d'exception dans function bar(). Notez également qu'il y aura la ligne:

During handling of the above exception, another exception occurred:

dans les messages de retraçage affichés.

# for Python 3.x
...
def bar(arg1):
    try:
        foo()
    except Exception as e:
        import sys
        raise type(e)(str(e) +
                      ' happens at %s' % arg1).with_traceback(sys.exc_info()[2])

bar('arg1')

Mise à jour 3

Un commentateur a demandé s'il y avait un moyen qui fonctionnerait à la fois dans Python 2 et 3. Bien que la réponse puisse sembler être "Non" en raison des différences de syntaxe, il existe un moyen de contourner cela en utilisant une fonction d'assistance comme reraise()dans l' sixadd- sur le module. Donc, si vous préférez ne pas utiliser la bibliothèque pour une raison quelconque, vous trouverez ci-dessous une version autonome simplifiée.

Notez aussi que puisque l'exception est relancée dans la reraise()fonction, cela apparaîtra dans tout ce que le retraçage est déclenché, mais le résultat final est ce que vous voulez.

import sys

if sys.version_info.major < 3:  # Python 2?
    # Using exec avoids a SyntaxError in Python 3.
    exec("""def reraise(exc_type, exc_value, exc_traceback=None):
                raise exc_type, exc_value, exc_traceback""")
else:
    def reraise(exc_type, exc_value, exc_traceback=None):
        if exc_value is None:
            exc_value = exc_type()
        if exc_value.__traceback__ is not exc_traceback:
            raise exc_value.with_traceback(exc_traceback)
        raise exc_value

def foo():
    try:
        raise IOError('Stuff')
    except:
        raise

def bar(arg1):
    try:
       foo()
    except Exception as e:
        reraise(type(e), type(e)(str(e) +
                                 ' happens at %s' % arg1), sys.exc_info()[2])

bar('arg1')
Martineau
la source
3
Cela perd la trace, en quelque sorte vaincre le point d'ajouter des informations à une exception existante. De plus, cela ne fonctionne pas avec les exceptions avec ctor qui prend> 1 arguments (le type est quelque chose que vous ne pouvez pas contrôler à partir de l'endroit où vous interceptez l'exception).
Václav Slavík
1
@ Václav: Il est assez facile d'éviter de perdre la trace - comme le montre la mise à jour que j'ai ajoutée. Bien que cela ne gère toujours pas toutes les exceptions imaginables, cela fonctionne pour des cas similaires à ce qui a été montré dans la question du PO.
martineau
1
Ce n'est pas tout à fait vrai. Si le type (e) remplace __str__, vous pouvez obtenir des résultats indésirables. Notez également que le deuxième argument est passé au constructeur donné par le premier argument, ce qui donne un peu de sens type(e)(type(e)(e.message). Troisièmement, e.message est obsolète au profit de e.args [0].
bukzor
1
donc, il n'y a pas de moyen portable qui fonctionne à la fois en Python 2 et 3?
Elias Dorneles
1
@martineau Quel est le but de l'importation à l'intérieur du bloc except? Est-ce pour économiser de la mémoire en n'important que lorsque cela est nécessaire?
AllTradesJack
116

Si vous êtes venu ici à la recherche d'une solution pour Python 3, le manuel dit:

Lors de la levée d'une nouvelle exception (plutôt que d'utiliser un bare raisepour relancer l'exception en cours de traitement), le contexte d'exception implicite peut être complété par une cause explicite en utilisant from with rise:

raise new_exc from original_exc

Exemple:

try:
    return [permission() for permission in self.permission_classes]
except TypeError as e:
    raise TypeError("Make sure your view's 'permission_classes' are iterable. "
                    "If you use '()' to generate a set with a single element "
                    "make sure that there is a comma behind the one (element,).") from e

Ce qui ressemble à ceci à la fin:

2017-09-06 16:50:14,797 [ERROR] django.request: Internal Server Error: /v1/sendEmail/
Traceback (most recent call last):
File "venv/lib/python3.4/site-packages/rest_framework/views.py", line 275, in get_permissions
    return [permission() for permission in self.permission_classes]
TypeError: 'type' object is not iterable 

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

Traceback (most recent call last):
    # Traceback removed...
TypeError: Make sure your view's Permission_classes are iterable. If 
     you use parens () to generate a set with a single element make 
     sure that there is a (comma,) behind the one element.

Transformer un totalement indéfinissable TypeErroren un joli message avec des indices vers une solution sans gâcher l'exception d'origine.

Chris
la source
14
C'est la meilleure solution, puisque l'exception qui en résulte renvoie à la cause d'origine, fournissez plus de détails.
JT
y a-t-il une solution pour que nous puissions ajouter un message sans toutefois soulever une nouvelle exception? Je veux dire simplement étendre le message de l'instance d'exception.
edcSam
Yaa ~~ ça marche, mais c'est comme quelque chose qui ne devrait pas être fait pour moi. Le message est stocké dans e.args, mais il s'agit d'un Tuple, il ne peut donc pas être modifié. Alors copiez-la d'abord argsdans une liste, puis modifiez-la, puis recopiez-la sous forme de tuple:args = list(e.args) args[0] = 'bar' e.args = tuple(args)
Chris
27

En supposant que vous ne voulez pas ou ne pouvez pas modifier foo (), vous pouvez faire ceci:

try:
    raise IOError('stuff')
except Exception as e:
    if len(e.args) >= 1:
        e.args = (e.args[0] + ' happens',) + e.args[1:]
    raise

C'est en effet la seule solution ici qui résout le problème dans Python 3 sans un message moche et déroutant "Lors de la gestion de l'exception ci-dessus, une autre exception s'est produite".

Dans le cas où la ligne de relance devrait être ajoutée à la trace de pile, écrire à la raise eplace de raisefera l'affaire.

Steve Howard
la source
mais dans ce cas, si l'exception change dans foo, je dois également changer de barre, non?
anijhaw
1
Si vous interceptez Exception (modifié ci-dessus), vous pouvez intercepter n'importe quelle exception de bibliothèque standard (ainsi que celles qui héritent d'Exception et appellent Exception .__ init__).
Steve Howard
6
pour être plus complet / coopératif, incluez les autres parties du tuple original:e.args = ('mynewstr' + e.args[0],) + e.args[1:]
Dubslow
1
@ nmz787 C'est la meilleure solution pour Python 3 en fait. Quelle est exactement votre erreur?
Christian
1
@Dubslow et martineau J'ai incorporé vos suggestions dans une édition.
Christian
9

Je n'aime pas toutes les réponses données jusqu'à présent. Ils sont encore trop verbeux à mon humble avis. Dans la sortie de code et de message.

Tout ce que je veux, c'est le stacktrace pointant vers l'exception source, pas de truc d'exception entre les deux, donc pas de création de nouvelles exceptions, il suffit de re-surélever l'original avec tous les états de frame de pile pertinents , qui y ont conduit.

Steve Howard a donné une belle réponse que je veux étendre, non, réduire ... à python 3 uniquement.

except Exception as e:
    e.args = ("Some failure state", *e.args)
    raise

La seule nouveauté est l' extension / décompression des paramètres qui le rend assez petit et assez facile à utiliser.

Essayez-le:

foo = None

try:
    try:
        state = "bar"
        foo.append(state)

    except Exception as e:
        e.args = ("Appending '"+state+"' failed", *e.args)
        raise

    print(foo[0]) # would raise too

except Exception as e:
    e.args = ("print(foo) failed: " + str(foo), *e.args)
    raise

Cela vous donnera:

Traceback (most recent call last):
  File "test.py", line 6, in <module>
    foo.append(state)
AttributeError: ('print(foo) failed: None', "Appending 'bar' failed", "'NoneType' object has no attribute 'append'")

Un joli joli imprimé pourrait être quelque chose comme

print("\n".join( "-"*i+" "+j for i,j in enumerate(e.args)))
6EQUJ5
la source
5

Une approche pratique que j'ai utilisée consiste à utiliser l'attribut de classe comme stockage pour les détails, car l'attribut de classe est accessible à la fois à partir de l'objet de classe et de l'instance de classe:

class CustomError(Exception):
    def __init__(self, details: Dict):
        self.details = details

Puis dans votre code:

raise CustomError({'data': 5})

Et lors de la détection d'une erreur:

except CustomError as e:
    # Do whatever you want with the exception instance
    print(e.details)
Kee
la source
Pas vraiment utile puisque l'OP demande que les détails soient imprimés dans le cadre de la trace de pile lorsque l'exception d'origine est levée et non interceptée.
cowbert
Je pense que la solution est bonne. Mais la description n'est pas vraie. Les attributs de classe sont copiés dans les instances lorsque vous les instanciez. Ainsi, lorsque vous modifiez l'attribut "détails" de l'instance, l'attribut de classe sera toujours None. Quoi qu'il en soit, nous voulons ce comportement ici.
Adam Wallner
2

Contrairement aux réponses précédentes, cela fonctionne face à des exceptions avec vraiment mauvais __str__. Il ne modifier le type cependant, afin de factoriser inutiles __str__mises en œuvre.

J'aimerais toujours trouver une amélioration supplémentaire qui ne modifie pas le type.

from contextlib import contextmanager
@contextmanager
def helpful_info():
    try:
        yield
    except Exception as e:
        class CloneException(Exception): pass
        CloneException.__name__ = type(e).__name__
        CloneException.__module___ = type(e).__module__
        helpful_message = '%s\n\nhelpful info!' % e
        import sys
        raise CloneException, helpful_message, sys.exc_traceback


class BadException(Exception):
    def __str__(self):
        return 'wat.'

with helpful_info():
    raise BadException('fooooo')

La trace et le type (nom) d'origine sont conservés.

Traceback (most recent call last):
  File "re_raise.py", line 20, in <module>
    raise BadException('fooooo')
  File "/usr/lib64/python2.6/contextlib.py", line 34, in __exit__
    self.gen.throw(type, value, traceback)
  File "re_raise.py", line 5, in helpful_info
    yield
  File "re_raise.py", line 20, in <module>
    raise BadException('fooooo')
__main__.BadException: wat.

helpful info!
Bukzor
la source
2

Je fournirai un extrait de code que j'utilise souvent chaque fois que je souhaite ajouter des informations supplémentaires à une exception. Je travaille à la fois en Python 2.7 et 3.6.

import sys
import traceback

try:
    a = 1
    b = 1j

    # The line below raises an exception because
    # we cannot compare int to complex.
    m = max(a, b)  

except Exception as ex:
    # I create my  informational message for debugging:
    msg = "a=%r, b=%r" % (a, b)

    # Gather the information from the original exception:
    exc_type, exc_value, exc_traceback = sys.exc_info()

    # Format the original exception for a nice printout:
    traceback_string = ''.join(traceback.format_exception(
        exc_type, exc_value, exc_traceback))

    # Re-raise a new exception of the same class as the original one, 
    # using my custom message and the original traceback:
    raise type(ex)("%s\n\nORIGINAL TRACEBACK:\n\n%s\n" % (msg, traceback_string))

Le code ci-dessus donne la sortie suivante:

---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-6-09b74752c60d> in <module>()
     14     raise type(ex)(
     15         "%s\n\nORIGINAL TRACEBACK:\n\n%s\n" %
---> 16         (msg, traceback_string))

TypeError: a=1, b=1j

ORIGINAL TRACEBACK:

Traceback (most recent call last):
  File "<ipython-input-6-09b74752c60d>", line 7, in <module>
    m = max(a, b)  # Cannot compare int to complex
TypeError: no ordering relation is defined for complex numbers


Je sais que cela s'écarte un peu de l'exemple fourni dans la question, mais j'espère néanmoins que quelqu'un le trouvera utile.

Pedro M Duarte
la source
1

Vous pouvez définir votre propre exception qui hérite d'une autre et créer son propre constructeur pour définir la valeur.

Par exemple:

class MyError(Exception):
   def __init__(self, value):
     self.value = value
     Exception.__init__(self)

   def __str__(self):
     return repr(self.value)
Alexandre Kiselev
la source
2
L'adresse n'a pas besoin de changer / ajouter quelque chose à l' messageexception d'origine (mais pourrait être corrigée, je pense).
martineau
-6

Peut être

except Exception as e:
    raise IOError(e.message + 'happens at %s'%arg1)
Malvolio
la source