Riscrivi l'eccezione con un tipo e un messaggio diversi, preservando le informazioni esistenti


139

Sto scrivendo un modulo e voglio avere una gerarchia di eccezioni unificata per le eccezioni che può sollevare (ad esempio ereditando da una FooErrorclasse astratta per tutte le fooeccezioni specifiche del modulo). Ciò consente agli utenti del modulo di catturare quelle particolari eccezioni e gestirle distintamente, se necessario. Ma molte delle eccezioni sollevate dal modulo sono sollevate a causa di qualche altra eccezione; ad es. errore in alcune attività a causa di un errore OSE su un file.

Ciò di cui ho bisogno è "racchiudere" l'eccezione rilevata in modo tale che abbia un tipo e un messaggio diversi , in modo che le informazioni siano disponibili più in alto nella gerarchia di propagazione da qualsiasi cosa catturi l'eccezione. Ma non voglio perdere il tipo, il messaggio e la traccia dello stack esistenti; sono tutte informazioni utili per qualcuno che cerca di eseguire il debug del problema. Un gestore di eccezioni di livello superiore non va bene, dal momento che sto cercando di decorare l'eccezione prima che raggiunga lo stack di propagazione e il gestore di livello superiore è troppo tardi.

Ciò è parzialmente risolto derivando fooi tipi di eccezione specifici del mio modulo dal tipo esistente (ad esempio class FooPermissionError(OSError, FooError)), ma ciò non semplifica il wrapping dell'istanza di eccezione esistente in un nuovo tipo, né modifica il messaggio.

PEP 3134 di Python "Eccezione concatenata e tracciati incorporati" discute una modifica accettata in Python 3.0 per "concatenare" oggetti eccezione, per indicare che è stata sollevata una nuova eccezione durante la gestione di un'eccezione esistente.

Quello che sto cercando di fare è correlato: ne ho bisogno che funzioni anche nelle precedenti versioni di Python e non ne ho bisogno per il concatenamento, ma solo per il polimorfismo. Qual è il modo giusto per farlo?


Le eccezioni sono già completamente polimorfiche - sono tutte sottoclassi di eccezione. Cosa stai cercando di fare? "Messaggio diverso" è abbastanza banale con un gestore di eccezioni di livello superiore. Perché stai cambiando classe?
S.Lott

Come spiegato nella domanda (ora, grazie per il tuo commento): sto cercando di decorare un'eccezione che ho catturato, in modo che possa propagarsi ulteriormente con più informazioni ma non perderne nessuna. Un gestore di livello superiore è troppo tardi.
Bignose,

Dai un'occhiata alla mia classe CausedException che può fare quello che vuoi in Python 2.x. Anche in Python 3 può essere utile nel caso in cui si desideri fornire più di un'eccezione originale come causa dell'eccezione. Forse si adatta alle tue esigenze.
Alfe,


Per python-2 faccio qualcosa di simile a @DevinJeanpierre ma sto solo aggiungendo un nuovo messaggio di stringa: except Exception as e-> raise type(e), type(e)(e.message + custom_message), sys.exc_info()[2]-> questa soluzione proviene da un'altra domanda SO . Questo non è carino ma funzionale.
Trevor Boyd Smith,

Risposte:


197

Python 3 ha introdotto il concatenamento delle eccezioni (come descritto in PEP 3134 ). Ciò consente, quando si genera un'eccezione, di citare un'eccezione esistente come "causa":

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

L'eccezione rilevata diventa quindi parte della (nuova "causa") della nuova eccezione ed è disponibile per qualsiasi codice catturi la nuova eccezione.

Usando questa funzione, l' __cause__attributo viene impostato. Il gestore di eccezioni incorporato sa anche come segnalare la "causa" e il "contesto" dell'eccezione insieme al traceback.


In Python 2 , questo caso d'uso non ha una buona risposta (come descritto da Ian Bicking e Ned Batchelder ). Bummer.


4
Ian Bicking non descrive la mia soluzione? Mi dispiace di aver dato una risposta così subdola, ma è strano che questa sia stata accettata.
Devin Jeanpierre,

1
@bignose Hai capito il mio punto non solo dall'avere ragione, ma per l'uso del "frobnicate" :)
David M.

5
Il concatenamento delle eccezioni è in realtà il comportamento predefinito ora, in realtà è l'opposto del problema, sopprimendo la prima eccezione che richiede lavoro, vedere PEP 409 python.org/dev/peps/pep-0409
Chris_Rands

1
Come realizzeresti questo in Python 2?
selotape,

1
Sembra funzionare benissimo (python 2.7)try: return 2 / 0 except ZeroDivisionError as e: raise ValueError(e)
alex

37

Puoi usare sys.exc_info () per ottenere il traceback e sollevare la tua nuova eccezione con detto traceback (come menziona PEP). Se vuoi preservare il vecchio tipo e messaggio, puoi farlo sull'eccezione, ma è utile solo se qualunque cosa catturi la tua eccezione lo cerca.

Per esempio

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

Certo, questo non è davvero così utile. Se lo fosse, non avremmo bisogno di quel PEP. Non consiglierei di farlo.


Devin, lì memorizzi un riferimento al traceback, non dovresti eliminare esplicitamente quel riferimento?
Arafangion,

2
Non ho archiviato nulla, ho lasciato traceback come variabile locale che presumibilmente non rientra nell'ambito. Sì, è possibile che non lo sia, ma se si sollevano eccezioni del genere nell'ambito globale anziché all'interno delle funzioni, si hanno problemi più grandi. Se il tuo reclamo è solo che potrebbe essere eseguito in un ambito globale, la soluzione corretta non è quella di aggiungere un boilerplate irrilevante che deve essere spiegato e non pertinente per il 99% degli usi, ma di riscrivere la soluzione in modo tale che è necessario mentre sembra che nulla sia diverso, come ho fatto ora.
Devin Jeanpierre,

4
Arafangion potrebbe riferirsi a un avvertimento nella documentazione di Python persys.exc_info() , @Devin. Dice "L'assegnazione del valore di ritorno del traceback a una variabile locale in una funzione che gestisce un'eccezione causerà un riferimento circolare". Tuttavia, una nota che segue afferma che a partire da Python 2.2 è possibile ripulire il ciclo, ma è più efficiente evitarlo.
Don Kirkby, il

5
Maggiori dettagli su diversi modi per rievocare le eccezioni in Python da due pitonisti illuminati: Ian Bicking e Ned Batchelder
Rodrigue

11

È possibile creare il proprio tipo di eccezione che si estende a prescindere dall'eccezione rilevata.

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

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

Ma la maggior parte delle volte, penso che sarebbe più semplice catturare l'eccezione, gestirla e raisel'eccezione originale (e preservare il traceback) oppure raise NewException(). Se avessi chiamato il tuo codice e avessi ricevuto una delle tue eccezioni personalizzate, mi aspetto che il tuo codice abbia già gestito l'eccezione che hai dovuto rilevare. Quindi non ho bisogno di accedervi da solo.

Modifica: ho trovato questa analisi dei modi per generare la tua eccezione e mantenere l'eccezione originale. Nessuna soluzione carina.


1
Il caso d'uso che ho descritto non è per la gestione dell'eccezione; si tratta in particolare di non gestirlo, ma di aggiungere alcune informazioni extra (una classe aggiuntiva e un nuovo messaggio) in modo che possano essere gestite ulteriormente nello stack di chiamate.
Bignose,

2

Ho anche scoperto che molte volte ho bisogno di un po 'di "avvolgimento" per errori sollevati.

Ciò includeva entrambi nell'ambito di una funzione e talvolta includeva solo alcune righe all'interno di una funzione.

Creato un wrapper da utilizzare a decoratore context manager:


Implementazione

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

Esempi di utilizzo

decoratore

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

quando si chiama il dometodo, non preoccuparti IndexError, soloMyError

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

gestore del contesto

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

all'interno do2, nel context managercaso in cui IndexErrorsia sollevato, sarà avvolto e sollevatoMyError


1
Spiegare cosa farà "wrapping" all'eccezione originale. Qual è lo scopo del tuo codice e quale comportamento abilita?
alexis

@alexis - aggiunti alcuni esempi, spero che sia d'aiuto
Aaron_ab il

-2

La soluzione più diretta alle tue esigenze dovrebbe essere questa:

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

In questo modo è possibile in seguito stampare il messaggio e l'errore specifico generato dalla funzione di caricamento


1
Ciò cattura e quindi elimina l'oggetto dell'eccezione, quindi no, non soddisfa le esigenze della domanda. La domanda chiede come mantenere l'eccezione esistente e consentire a quella stessa eccezione, con tutte le informazioni utili in essa contenute, di continuare a propagare lo stack.
bignose il
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.