Aggiunta di informazioni a un'eccezione?


144

Voglio ottenere qualcosa del genere:

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')

Ma quello che ottengo è:

Traceback..
  IOError('Stuff')

Qualche indizio su come raggiungere questo obiettivo? Come farlo sia in Python 2 che in 3?


Durante la ricerca della documentazione per l' messageattributo Exception ho trovato questa domanda SO, BaseException.message deprecata in Python 2.6 , che sembra indicare che il suo uso è ora sconsigliato (e perché non è nei documenti).
martineau,

purtroppo, quel collegamento sembra non funzionare più.
Michael Scott Cuthbert,

1
@MichaelScottCuthbert ecco una buona alternativa: itmaybeahack.com/book/python-2.6/html/p02/…
Niels Keurentjes

Ecco una spiegazione davvero valida di quale sia lo stato dell'attributo message e della sua relazione con l'attributo args e PEP 352 . È tratto dal libro gratuito Building Skills in Python di Steven F. Lott.
martineau,

Risposte:


119

Lo farei così, quindi cambiando il suo tipo foo()non è necessario cambiarlo 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

Aggiornamento 1

Ecco una leggera modifica che preserva il traceback originale:

...
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

Aggiornamento 2

Per Python 3.x, il codice nel mio primo aggiornamento è sintatticamente errato e l'idea di avere un messageattributo attivo èBaseException stata ritirata in una modifica a PEP 352 il 16/05/2012 (il mio primo aggiornamento è stato pubblicato il 12-03-2012) . Quindi attualmente, in Python 3.5.2, avresti comunque bisogno di fare qualcosa lungo queste linee per preservare il traceback e non codificare il tipo di eccezione in funzione bar(). Si noti inoltre che ci sarà la linea:

During handling of the above exception, another exception occurred:

nei messaggi di traceback visualizzati.

# 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')

Aggiornamento 3

Un commentatore ha chiesto se ci fosse un modo che avrebbe funzionato sia in Python 2 e 3. Anche se la risposta potrebbe sembrare di essere "No" a causa delle differenze di sintassi, c'è un modo per aggirare che, utilizzando una funzione di supporto, come reraise()nel sixadd sul modulo. Quindi, se preferisci non utilizzare la libreria per qualche motivo, di seguito è una versione standalone semplificata.

Si noti inoltre che, poiché l'eccezione viene rielaborata all'interno della reraise()funzione, ciò verrà visualizzato in qualunque traceback generato, ma il risultato finale è quello desiderato.

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')

3
Ciò perde il backtrace, in un certo senso sconfiggendo il punto di aggiungere informazioni a un'eccezione esistente. Inoltre, non funziona eccezioni con ctor che accetta> 1 argomenti (il tipo è qualcosa che non puoi controllare dal punto in cui prendi l'eccezione).
Václav Slavík,

1
@Václav: è abbastanza facile prevenire la perdita del backtrace - come mostrato nell'aggiornamento che ho aggiunto. Sebbene ciò non gestisca ancora ogni possibile eccezione, funziona per casi simili a quelli mostrati nella domanda del PO.
martineau,

1
Questo non è del tutto giusto. Se il tipo (e) ha la precedenza __str__, potresti ottenere risultati indesiderati. Si noti inoltre che il secondo argomento viene passato al costruttore dato dal primo argomento, che produce un po 'senza senso type(e)(type(e)(e.message). In terzo luogo, e.message è deprecato a favore di e.args [0].
Bukzor,

1
quindi, non esiste un modo portatile che funzioni in Python 2 e 3?
Elias Dorneles,

1
@martineau Qual è lo scopo dell'importazione all'interno del blocco tranne? Questo consente di risparmiare memoria importando solo quando necessario?
AllTradesJack,

116

Nel caso in cui venissi qui alla ricerca di una soluzione per Python 3, il manuale dice:

Quando si solleva una nuova eccezione (anziché utilizzare uno bare raiseper ri-sollevare l'eccezione attualmente gestita), il contesto di eccezione implicita può essere integrato con una causa esplicita utilizzando from with raise:

raise new_exc from original_exc

Esempio:

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

Che assomiglia a questo alla fine:

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.

Trasformare un totalmente anonimo TypeErrorin un bel messaggio con suggerimenti per una soluzione senza incasinare l'eccezione originale.


14
Questa è la soluzione migliore, poiché l'eccezione risultante punta alla causa originale, fornisce maggiori dettagli.
JT

c'è qualche soluzione che possiamo aggiungere qualche messaggio ma ancora non sollevare una nuova eccezione? Intendo semplicemente estendere il messaggio dell'istanza di eccezione.
edcSam,

Yaa ~~ funziona, ma sembra qualcosa che non dovrebbe essere fatto a me. Il messaggio è memorizzato in e.args, ma si tratta di una Tupla, quindi non può essere modificato. Quindi prima copia argsin un elenco, quindi modificalo, quindi copialo di nuovo come una Tupla:args = list(e.args) args[0] = 'bar' e.args = tuple(args)
Chris

27

Supponendo che tu non voglia o non possa modificare foo (), puoi farlo:

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

Questa è davvero l'unica soluzione qui che risolve il problema in Python 3 senza un brutto e confuso messaggio "Durante la gestione dell'eccezione precedente, si è verificata un'altra eccezione".

Nel caso in cui la linea di rialzo debba essere aggiunta alla traccia dello stack, scrivere raise einvece di raisefare il trucco.


ma in questo caso se l'eccezione cambia in pippo, devo cambiare anche la barra, giusto?
anijhaw,

1
Se si rileva un'eccezione (modificata in precedenza), è possibile rilevare qualsiasi eccezione di libreria standard (nonché quelle che ereditano da Exception e chiamano Exception .__ init__).
Steve Howard,

6
per essere più completo / cooperativo, includi le altre parti della tupla originale:e.args = ('mynewstr' + e.args[0],) + e.args[1:]
Dubslow

1
@ nmz787 Questa è la migliore soluzione per Python 3. Qual è esattamente il tuo errore?
Christian,

1
@Dubslow e martineau ho incorporato i tuoi suggerimenti in una modifica.
Christian,

9

Finora non mi piacciono tutte le risposte fornite. Sono ancora un imbo troppo prolisso. In entrambi i codici e output dei messaggi.

Tutto quello che voglio avere è lo stacktrace che punta all'eccezione di origine, nessuna roba di eccezione in mezzo, quindi nessuna creazione di nuove eccezioni, solo il rilancio dell'originale con tutti i relativi stati del frame dello stack che ha portato lì.

Steve Howard ha dato una bella risposta che voglio estendere, no, ridurre ... solo a Python 3.

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

L'unica novità è l' espansione / decompressione dei parametri che lo rende piccolo e abbastanza facile da usare.

Provalo:

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

Questo ti darà:

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'")

Una semplice bella stampa potrebbe essere qualcosa di simile

print("\n".join( "-"*i+" "+j for i,j in enumerate(e.args)))

5

Un approccio utile che ho usato è usare l'attributo di classe come memoria per i dettagli, poiché l'attributo di classe è accessibile sia dall'oggetto di classe che dall'istanza di classe:

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

Quindi nel tuo codice:

raise CustomError({'data': 5})

E quando si rileva un errore:

except CustomError as e:
    # Do whatever you want with the exception instance
    print(e.details)

Non molto utile poiché l'OP richiede che i dettagli vengano stampati come parte della traccia dello stack quando viene generata e non rilevata l'eccezione originale.
cowbert,

Penso che la soluzione sia buona. Ma la descrizione non è vera. Gli attributi di classe vengono copiati nelle istanze quando vengono istanziati. Pertanto, quando si modifica l'attributo "dettagli", dell'istanza, l'attributo di classe sarà comunque Nessuno. Comunque vogliamo questo comportamento qui.
Adam Wallner,

2

A differenza delle risposte precedenti, questo funziona di fronte alle eccezioni con davvero male __str__. Si fa modificare il tipo tuttavia, al fine di fattore fuori inutili __str__implementazioni.

Mi piacerebbe ancora trovare un ulteriore miglioramento che non modifichi il tipo.

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')

Il traceback e il tipo (nome) originali vengono conservati.

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!

2

Fornirò uno snippet di codice che utilizzo spesso ogni volta che voglio aggiungere ulteriori informazioni a un'eccezione. Lavoro sia in Python 2.7 che 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))

Il codice sopra riportato produce il seguente output:

---------------------------------------------------------------------------
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


So che questo si discosta leggermente dall'esempio fornito nella domanda, ma spero comunque che qualcuno lo trovi utile.


1

Puoi definire la tua eccezione che eredita da un'altra e creare il proprio costruttore per impostare il valore.

Per esempio:

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

   def __str__(self):
     return repr(self.value)

2
Non è necessario modificare / aggiungere qualcosa messageall'eccezione originale (ma potrebbe essere risolto, credo).
martineau,

-6

Può essere

except Exception as e:
    raise IOError(e.message + 'happens at %s'%arg1)
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.