web-dev-qa-db-fra.com

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

J'écris un module et je veux avoir une hiérarchie d'exceptions unifiée pour les exceptions qu'il peut déclencher (par exemple, hériter d'une classe abstraite FooError pour toutes les exceptions spécifiques du module foo). Cela permet aux utilisateurs du module d'attraper ces exceptions particulières et de les gérer distinctement, si nécessaire. Mais de nombreuses exceptions levées à partir du module sont levées à cause d'une autre exception; par exemple. échec à une tâche en raison d'une OSError sur un fichier.

Ce dont j'ai besoin est de "encapsuler" l'exception intercepté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 fait 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 progresse 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 types d'exceptions spécifiques de mon module foo 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 tapez, ni modifiez le message.

Python PEP 3134 "Chaînage d'exceptions et traces intégrées" décrit une modification acceptée dans Python 3.0 pour les objets d'exception de "chaînage", pour indiquer qu'une nouvelle exception a été levée pendant la traitement d'une exception existante.

Ce que j'essaie de faire est lié: j'ai besoin qu'il fonctionne également dans les versions antérieures Python, et je n'en ai pas besoin pour le chaînage, mais uniquement pour le polymorphisme. Quelle est la bonne façon de le faire ?

116
bignose

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 partie de (est la "cause") de la nouvelle exception, et est disponible pour tout code qui intercepte la nouvelle exception.

En utilisant cette fonction, le __cause__ l'attribut est défini. Le gestionnaire d'exceptions intégré aussi sait comment rapporter la "cause" et le "contexte" de l'exception avec le traceback.


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.

160
bignose

Vous pouvez utiliser sys.exc_info () pour obtenir le traceback, et déclencher votre nouvelle exception avec ledit traceback (comme le PEP le mentionne). Si vous souhaitez conserver l'ancien type et le message, vous pouvez le faire sur l'exception, mais cela 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 très utile. Si c'était le cas, nous n'aurions pas besoin de ce PEP. Je ne recommanderais pas de le faire.

35
Devin Jeanpierre

Vous pouvez créer votre propre type d'exception qui s'étend quelle que soit l'exception que vous avez détecté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 raise l'exception d'origine (et de conserver la trace) 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é l'exception que vous avez dû intercepter. Ainsi, je n'ai pas besoin d'y accéder moi-même.

Edit: j'ai trouvé cette analyse de façons de lever votre propre exception et de conserver l'exception d'origine. Pas de jolies solutions.

11
Nikhil Chelliah

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

Cela inclus à la fois dans une portée de fonction et parfois envelopper seulement certaines lignes à l'intérieur d'une fonction.

Créé un wrapper pour être utilisé un decorator et 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 méthode do, ne vous inquiétez pas pour 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é et levé MyError

2
Aaron_ab