197 votes

Reprise de l'exception avec un type et un message différents, tout en conservant les informations existantes

J'écris un module et je souhaite avoir une hiérarchie d'exceptions unifiée pour les exceptions qu'il peut soulever (par exemple, en héritant d'une exception de type FooError classe abstraite pour tous les foo les exceptions spécifiques du module). Cela permet aux utilisateurs du module d'attraper ces exceptions particulières et de les traiter séparément, si nécessaire. Mais la plupart des exceptions soulevées par le module le sont à cause d'une autre exception ; par exemple, l'échec d'une tâche à cause d'une OSError sur un fichier.

Ce dont j'ai besoin, c'est de "envelopper" l'exception capturée de manière à ce qu'elle ait un type et un message différents afin que l'information soit disponible plus haut dans la hiérarchie de propagation, quel que soit l'élément qui capture 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'exception de niveau supérieur n'est pas bon, puisque j'essaie de décorer l'exception avant qu'elle ne remonte la pile de propagation, et que le gestionnaire de niveau supérieur arrive trop tard.

Ce problème est en partie résolu par la dérivation de mon module foo des types d'exception spécifiques du type existant (par ex. class FooPermissionError(OSError, FooError) ), mais cela ne facilite pas l'intégration de l'instance d'exception existante dans un nouveau type, ni la modification du message.

Python PEP 3134 "Exception Chaining and Embedded Tracebacks" traite d'un changement accepté dans Python 3.0 pour le "chaînage" des objets d'exception, afin d'indiquer qu'une nouvelle exception a été soulevée pendant le traitement d'une exception existante.

Ce que j'essaie de faire est lié : J'ai besoin que cela fonctionne aussi dans les versions antérieures de Python, et j'en ai besoin non pas pour le chaînage, mais seulement pour le polymorphisme. Quelle est la bonne façon de procéder ?

314voto

bignose Points 6573

Python 3 introduite chaînage d'exceptions (tel que décrit dans PEP 3134 ). Cela permet, lorsqu'on soulève une exception, de citer une exception existante comme "cause" :

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

L'exception levée ( exc (une KeyError) devient ainsi partie intégrante (est la "cause") de la nouvelle exception, une ValueError. La "cause" est disponible pour tout code qui attrape la nouvelle exception.

En utilisant cette fonction, le __cause__ est défini. Le gestionnaire d'exception intégré sait comment signaler la "cause" et le "contexte" de l'exception ainsi que la traceback.


En Python 2 il semble que ce cas d'utilisation n'ait pas de bonne réponse (comme décrit par Ian Bicking y Ned Batchelder ). Le pire est à venir.

40voto

Devin Jeanpierre Points 23162

Vous pouvez utiliser sys.exc_info() pour obtenir la traceback, et lever votre nouvelle exception avec cette traceback (comme le mentionne le PEP). Si vous voulez préserver l'ancien type et l'ancien message, vous pouvez le faire sur l'exception, mais cela n'est utile que si ce qui attrape 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 pas très utile. Si c'était le cas, nous n'aurions pas besoin de cette PEP. Je ne recommande pas de le faire.

16voto

Nikhil Chelliah Points 3463

Vous pourriez créer votre propre type d'exception qui étendrait quelle que soit l'exception que vous avez attrapé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 traiter, et de soit raise l'exception originale (et préserver la traceback) ou raise NewException() . Si j'appelle votre code et que je reçois l'une de vos exceptions personnalisées, je m'attends à ce que votre code ait déjà géré l'exception que vous deviez attraper. Je n'ai donc pas besoin d'y accéder moi-même.

Edit : J'ai trouvé cette analyse des façons de lancer votre propre exception et de conserver l'exception d'origine. Il n'y a pas de belles solutions.

6voto

Aaron_ab Points 1290

J'ai également constaté que j'avais souvent besoin d'un "emballage" pour les erreurs soulevées.

Cela inclut à la fois le champ d'application d'une fonction et parfois seulement quelques lignes à l'intérieur d'une fonction.

Création d'une enveloppe destinée à être utilisée par un decorator y context manager :


Mise en œuvre

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 do ne vous préoccupez pas de la méthode IndexError , juste MyError

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 si IndexError est levé, il sera enveloppée et levée MyError

1voto

Dan Points 186

Cela n'a rien à voir, mais lors de l'élaboration de messages d'erreur cohérents pour les ma propre bibliothèque J'ai constaté que nous enveloppions nos propres messages d'erreur au fur et à mesure qu'ils remontaient. J'ai été heureux de voir que Python 3.11 offre maintenant une fonction add_note pour compléter une erreur existante avec des informations supplémentaires, ce qui pourrait également être utile.

Pour Python 3.11 add_note()

La méthode add_note() est ajoutée à BaseException. Elle peut être utilisée pour enrichir les exceptions d'informations contextuelles qui ne sont pas au moment où l'exception est levée. Les notes ajoutées apparaissent dans le champ par défaut.

Nous suivons donc ce modèle :

try:
   some_risky_business()
except MyCustomException as ce:
  ce.add_note(f"Here is some more critical context!")
  raise se
except Exception as e:
  raise MyCustomException("Wow, didn't expect this error.") from e

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X