Relancer l'exception avec un type et un message différents, en préservant les informations existantes

139

J'écris un module et je souhaite avoir une hiérarchie d'exceptions unifiée pour les exceptions qu'il peut soulever (par exemple, hériter d'une FooErrorclasse abstraite pour toutes les fooexceptions spécifiques du module). Cela permet aux utilisateurs du module d'attraper ces exceptions particulières et de les gérer distinctement, si nécessaire. Mais la plupart des exceptions levées à partir du module sont déclenchées à cause d'une autre exception; par exemple, échec d'une tâche à cause d'une erreur OSError sur un fichier.

Ce dont j'ai besoin, c'est «d'envelopper» l'exception capturée de telle sorte qu'elle ait un type et un message différents , de sorte que les informations soient disponibles plus haut dans la hiérarchie de propagation par tout ce qui intercepte l'exception. Mais je ne veux pas perdre le type, le message et la trace de pile existants; ce sont toutes des informations utiles pour quelqu'un qui essaie de déboguer le problème. Un gestionnaire d'exceptions de niveau supérieur n'est pas bon, car j'essaie de décorer l'exception avant qu'elle ne monte plus haut dans la pile de propagation, et le gestionnaire de niveau supérieur est trop tard.

Ceci est en partie résolu en dérivant les footypes d'exceptions spécifiques de mon module à partir du type existant (par exemple class FooPermissionError(OSError, FooError)), mais cela ne facilite pas l'encapsulation de l'instance d'exception existante dans un nouveau type, ni ne modifie le message.

Le PEP 3134 de Python «Chaînage d'exceptions et traçages intégrés» traite d'un changement accepté dans Python 3.0 pour le «chaînage» des objets d'exception, pour indiquer qu'une nouvelle exception a été déclenchée lors de la gestion d'une exception existante.

Ce que j'essaie de faire est lié: j'en ai besoin aussi pour les versions antérieures de Python, et j'en ai besoin non pas pour le chaînage, mais uniquement pour le polymorphisme. Quel est le bon moyen de le faire?

gros nez
la source
Les exceptions sont déjà complètement polymorphes - ce sont toutes des sous-classes d'Exception. Qu'essayez-vous de faire? «Message différent» est assez simple avec un gestionnaire d'exceptions de niveau supérieur. Pourquoi changez-vous de classe?
S.Lott
Comme expliqué dans la question (maintenant, merci pour votre commentaire): J'essaie de décorer une exception que j'ai attrapée, afin qu'elle puisse se propager plus loin avec plus d'informations sans en perdre. Un gestionnaire de niveau supérieur est trop tard.
bignose
Veuillez jeter un œil à ma classe CausedException qui peut faire ce que vous voulez dans Python 2.x. Également dans Python 3, il peut être utile au cas où vous voudriez donner plus d'une exception d'origine comme cause de votre exception. Peut-être que cela répond à vos besoins.
Alfe
Pour python-2, je fais quelque chose de similaire à @DevinJeanpierre mais j'ajoute juste un nouveau message de chaîne: except Exception as e-> raise type(e), type(e)(e.message + custom_message), sys.exc_info()[2]-> cette solution provient d'une autre question SO . Ce n'est pas joli mais fonctionnel.
Trevor Boyd Smith

Réponses:

197

Python 3 a introduit le chaînage d'exceptions (comme décrit dans PEP 3134 ). Cela permet, lors de la levée d'une exception, de citer une exception existante comme «cause»:

try:
    frobnicate()
except KeyError as exc:
    raise ValueError("Bad grape") from exc

L'exception interceptée devient ainsi une partie de (est la «cause de») la nouvelle exception, et est disponible pour tout code qui intercepte la nouvelle exception.

En utilisant cette fonctionnalité, l' __cause__attribut est défini. Le gestionnaire d'exceptions intégré sait également comment signaler la «cause» et le «contexte» de l'exception avec le traçage.


Dans Python 2 , il semble que ce cas d'utilisation n'ait pas de bonne réponse (comme décrit par Ian Bicking et Ned Batchelder ). Bummer.

gros nez
la source
4
Ian Bicking ne décrit-il pas ma solution? Je regrette d'avoir donné une réponse aussi honnête, mais c'est bizarre que celle-ci ait été acceptée.
Devin Jeanpierre le
1
@bignose Vous avez compris mon point non seulement d'avoir raison, mais pour l'utilisation de "frobnicate" :)
David M.
5
Le chaînage d'exceptions est en fait le comportement par défaut maintenant, en fait c'est l'opposé du problème, en supprimant la première exception qui nécessite du travail, voir PEP 409 python.org/dev/peps/pep-0409
Chris_Rands
1
Comment feriez-vous cela en python 2?
Selotape
1
Cela semble fonctionner correctement (python 2.7)try: return 2 / 0 except ZeroDivisionError as e: raise ValueError(e)
alex
37

Vous pouvez utiliser sys.exc_info () pour obtenir le traçage, et lever votre nouvelle exception avec ledit traçage (comme le PEP le mentionne). Si vous souhaitez conserver l'ancien type et le message, vous pouvez le faire sur l'exception, mais ce n'est utile que si ce qui intercepte votre exception le recherche.

Par exemple

import sys

def failure():
    try: 1/0
    except ZeroDivisionError, e:
        type, value, traceback = sys.exc_info()
        raise ValueError, ("You did something wrong!", type, value), traceback

Bien sûr, ce n'est vraiment pas si utile. Si c'était le cas, nous n'aurions pas besoin de ce PEP. Je ne recommanderais pas de le faire.

Devin Jeanpierre
la source
Devin, vous y stockez une référence à la traceback, ne devriez-vous pas supprimer explicitement cette référence?
Arafangion
2
Je n'ai rien stocké, j'ai laissé traceback comme une variable locale qui tombe probablement hors de portée. Oui, il est concevable que ce ne soit pas le cas, mais si vous soulevez des exceptions comme celle-ci dans la portée globale plutôt que dans les fonctions, vous avez de plus gros problèmes. Si votre plainte est seulement qu'elle pourrait être exécutée dans une portée globale, la solution appropriée n'est pas d'ajouter des passe-partout non pertinents qui doivent être expliqués et qui ne sont pas pertinents pour 99% des utilisations, mais de réécrire la solution pour que rien de tel est nécessaire tout en donnant l'impression que rien n'est différent - comme je l'ai fait maintenant.
Devin Jeanpierre
4
Arafangion fait peut-être référence à un avertissement dans la documentation Python poursys.exc_info() @Devin. Il dit: «L'affectation de la valeur de retour de trace à une variable locale dans une fonction qui gère une exception provoquera une référence circulaire». Cependant, une note suivante indique que depuis Python 2.2, le cycle peut être nettoyé, mais il est plus efficace de l'éviter.
Don Kirkby
5
Plus de détails sur les différentes façons de re-soulever des exceptions en Python auprès de deux pythonistes éclairés: Ian Bicking et Ned Batchelder
Rodrigue
11

Vous pouvez créer votre propre type d'exception qui étend l'exception que vous avez interceptée.

class NewException(CaughtException):
    def __init__(self, caught):
        self.caught = caught

try:
    ...
except CaughtException as e:
    ...
    raise NewException(e)

Mais la plupart du temps, je pense qu'il serait plus simple d'attraper l'exception, de la gérer et de raisel'exception d'origine (et de conserver le retraçage) ou raise NewException(). Si j'appelais votre code et que je recevais l'une de vos exceptions personnalisées, je m'attendrais à ce que votre code ait déjà géré toute exception que vous avez dû intercepter. Je n'ai donc pas besoin d'y accéder moi-même.

Edit: J'ai trouvé cette analyse des moyens de lever votre propre exception et de conserver l'exception d'origine. Pas de jolies solutions.

Nikhil Chelliah
la source
1
Le cas d'utilisation que j'ai décrit n'est pas pour gérer l'exception; il s'agit spécifiquement de ne pas le gérer, mais d'ajouter des informations supplémentaires (une classe supplémentaire et un nouveau message) afin qu'il puisse être traité plus haut dans la pile d'appels.
bignose
2

J'ai également constaté que plusieurs fois, j'ai besoin d'un "emballage" aux erreurs soulevées.

Cela incluait à la fois la portée d'une fonction et n'enveloppait parfois que quelques lignes à l'intérieur d'une fonction.

Créé un wrapper à utiliser a decoratoret context manager:


la mise en oeuvre

import inspect
from contextlib import contextmanager, ContextDecorator
import functools    

class wrap_exceptions(ContextDecorator):
    def __init__(self, wrapper_exc, *wrapped_exc):
        self.wrapper_exc = wrapper_exc
        self.wrapped_exc = wrapped_exc

    def __enter__(self):
        pass

    def __exit__(self, exc_type, exc_val, exc_tb):
        if not exc_type:
            return
        try:
            raise exc_val
        except self.wrapped_exc:
            raise self.wrapper_exc from exc_val

    def __gen_wrapper(self, f, *args, **kwargs):
        with self:
            for res in f(*args, **kwargs):
                yield res

    def __call__(self, f):
        @functools.wraps(f)
        def wrapper(*args, **kw):
            with self:
                if inspect.isgeneratorfunction(f):
                    return self.__gen_wrapper(f, *args, **kw)
                else:
                    return f(*args, **kw)
        return wrapper

Exemples d'utilisation

décorateur

@wrap_exceptions(MyError, IndexError)
def do():
   pass

lors de l'appel de la dométhode, ne vous inquiétez pas IndexError, justeMyError

try:
   do()
except MyError as my_err:
   pass # handle error 

gestionnaire de contexte

def do2():
   print('do2')
   with wrap_exceptions(MyError, IndexError):
       do()

à l'intérieur do2, dans le context manager, siIndexError est soulevé, il sera enveloppé et soulevéMyError

Aaron_ab
la source
1
Veuillez expliquer ce que le «wrapping» fera à l'exception d'origine. Quel est le but de votre code et quel comportement permet-il?
alexis
@alexis - a ajouté quelques exemples, j'espère que cela aidera
Aaron_ab
-2

La solution la plus simple à vos besoins devrait être la suivante:

try:
     upload(file_id)
except Exception as upload_error:
     error_msg = "Your upload failed! File: " + file_id
     raise RuntimeError(error_msg, upload_error)

De cette façon, vous pouvez imprimer ultérieurement votre message et l'erreur spécifique générée par la fonction de téléchargement

funny_dev
la source
1
Cela attrape puis jette l'objet d'exception, donc non, cela ne répond pas aux besoins de la question. La question demande comment conserver l'exception existante et permettre à cette même exception, avec toutes les informations utiles qu'elle contient, de continuer à se propager dans la pile.
bignose