Modo corretto per dichiarare eccezioni personalizzate nel moderno Python?


1291

Qual è il modo corretto di dichiarare le classi di eccezione personalizzate nel moderno Python? Il mio obiettivo principale è quello di seguire qualunque altra classe di eccezione standard, in modo che (ad esempio) qualsiasi stringa aggiuntiva che includo nell'eccezione venga stampata da qualunque strumento abbia rilevato l'eccezione.

Per "moderno Python" intendo qualcosa che verrà eseguito in Python 2.5 ma che sia 'corretto' per il modo di fare le cose in Python 2.6 e Python 3. *. E per "personalizzato" intendo un oggetto Eccezione che può includere dati extra sulla causa dell'errore: una stringa, forse anche qualche altro oggetto arbitrario rilevante per l'eccezione.

Sono stato inciampato dal seguente avviso di deprecazione in Python 2.6.2:

>>> class MyError(Exception):
...     def __init__(self, message):
...         self.message = message
... 
>>> MyError("foo")
_sandbox.py:3: DeprecationWarning: BaseException.message has been deprecated as of Python 2.6

Sembra folle che BaseExceptionabbia un significato speciale per gli attributi nominati message. Racconto da PEP-352 che l'attributo aveva un significato speciale in 2.5 che stanno cercando di deprezzare, quindi immagino che quel nome (e quello solo) sia ora proibito? Ugh.

Sono anche consapevole che Exceptionha alcuni parametri magici args, ma non ho mai saputo come usarlo. Né sono sicuro che sia il modo giusto di fare le cose in futuro; molte delle discussioni che ho trovato online suggerivano che stavano cercando di eliminare args in Python 3.

Aggiornamento: due risposte hanno suggerito di ignorare __init__e __str__/ __unicode__/ __repr__. Sembra un sacco di battitura, è necessario?

Risposte:


1323

Forse ho perso la domanda, ma perché no:

class MyException(Exception):
    pass

Modifica: per sovrascrivere qualcosa (o passare argomenti extra), procedere come segue:

class ValidationError(Exception):
    def __init__(self, message, errors):

        # Call the base class constructor with the parameters it needs
        super(ValidationError, self).__init__(message)

        # Now for your custom code...
        self.errors = errors

In questo modo è possibile passare la dettatura dei messaggi di errore al secondo parametro e accedervi in ​​seguito e.errors


Aggiornamento di Python 3: in Python 3+, puoi usare questo uso leggermente più compatto di super():

class ValidationError(Exception):
    def __init__(self, message, errors):

        # Call the base class constructor with the parameters it needs
        super().__init__(message)

        # Now for your custom code...
        self.errors = errors

35
Tuttavia, un'eccezione definita come questa non sarebbe selezionabile; vedere la discussione qui stackoverflow.com/questions/16244923/...~~V~~singular~~3rd
Jiakai

87
@jiakai significa "selezionabile". :-)
Robino,

1
A seguito della documentazione di Python per le eccezioni definite dall'utente, i nomi menzionati nella funzione __init__ non sono corretti. Invece di (sé, messaggio, errore) è (sé, espressione, messaggio). L'espressione dell'attributo è l'espressione di input in cui si è verificato l'errore e il messaggio è una spiegazione dell'errore.
Ddleon,

2
Questo è un malinteso, @ddleon. L'esempio nei documenti a cui ti riferisci è per un caso d'uso particolare. Non ha significato il nome degli argomenti del costruttore della sottoclasse (né il loro numero).
asthasr,

498

Con le moderne eccezioni di Python, non è necessario abusare .message, sovrascrivere .__str__()o parte .__repr__()di esso. Se tutto ciò che desideri è un messaggio informativo quando viene sollevata la tua eccezione, procedere come segue:

class MyException(Exception):
    pass

raise MyException("My hovercraft is full of eels")

Questo darà un traceback che termina con MyException: My hovercraft is full of eels.

Se si desidera maggiore flessibilità dall'eccezione, è possibile passare un dizionario come argomento:

raise MyException({"message":"My hovercraft is full of animals", "animal":"eels"})

Tuttavia, ottenere quei dettagli in un exceptblocco è un po 'più complicato. I dettagli sono memorizzati argsnell'attributo, che è un elenco. Dovresti fare qualcosa del genere:

try:
    raise MyException({"message":"My hovercraft is full of animals", "animal":"eels"})
except MyException as e:
    details = e.args[0]
    print(details["animal"])

È ancora possibile passare più elementi all'eccezione e accedervi tramite indici di tupla, ma questo è altamente scoraggiato (ed è stato addirittura inteso per la deprecazione qualche tempo fa). Se hai bisogno di più di una singola informazione e il metodo sopra non è sufficiente per te, allora dovresti sottoclassare Exceptioncome descritto nel tutorial .

class MyError(Exception):
    def __init__(self, message, animal):
        self.message = message
        self.animal = animal
    def __str__(self):
        return self.message

2
"ma questo sarà deprecato in futuro" - è ancora destinato all'ammortamento? Python 3.7 sembra ancora accettarlo felicemente Exception(foo, bar, qux).
mtraceur,

Non ha visto alcun lavoro recente per privarlo dall'ultimo tentativo fallito a causa del dolore della transizione, ma tale utilizzo è ancora scoraggiato. Aggiornerò la mia risposta per riflettere ciò.
frnknstn,

@frnknstn, perché è scoraggiato? Mi sembra un bel linguaggio.
neves

2
@neves per iniziare, l'uso delle tuple per memorizzare le informazioni sulle eccezioni non ha alcun vantaggio rispetto all'utilizzo di un dizionario per fare lo stesso. Se sei interessato al ragionamento alla base delle modifiche alle eccezioni, dai un'occhiata a PEP352
frnknstn,

La sezione pertinente di PEP352 è "Idee ritratte " .
liberforce,

196

"Modo corretto per dichiarare eccezioni personalizzate nel moderno Python?"

Questo va bene, a meno che la tua eccezione non sia in realtà un tipo di eccezione più specifica:

class MyException(Exception):
    pass

O meglio (forse perfetto), invece di passdare una dotstring:

class MyException(Exception):
    """Raise for my specific kind of exception"""

Sottoclassi Sottoclassi di eccezione

Dai documenti

Exception

Tutte le eccezioni integrate, non esistenti nel sistema, derivano da questa classe. Anche tutte le eccezioni definite dall'utente devono essere derivate da questa classe.

Ciò significa che se la tua eccezione è un tipo di eccezione più specifica, esegui la sottoclasse di tale eccezione anziché quella generica Exception(e il risultato sarà che continuerai a derivare Exceptioncome raccomandato dai documenti). Inoltre, puoi almeno fornire un docstring (e non essere obbligato a usare la passparola chiave):

class MyAppValueError(ValueError):
    '''Raise when my specific value is wrong'''

Imposta gli attributi che ti crei con un'abitudine __init__. Evita di passare un dict come argomento posizionale, i futuri utenti del tuo codice ti ringrazieranno. Se si utilizza l'attributo del messaggio obsoleto, assegnandolo da soli si eviterà un DeprecationWarning:

class MyAppValueError(ValueError):
    '''Raise when a specific subset of values in context of app is wrong'''
    def __init__(self, message, foo, *args):
        self.message = message # without this you may get DeprecationWarning
        # Special attribute you desire with your Error, 
        # perhaps the value that caused the error?:
        self.foo = foo         
        # allow users initialize misc. arguments as any other builtin Error
        super(MyAppValueError, self).__init__(message, foo, *args) 

Non c'è davvero bisogno di scrivere il tuo __str__o __repr__. Quelli incorporati sono molto belli e la tua eredità cooperativa ti assicura di usarlo.

Critica della risposta migliore

Forse ho perso la domanda, ma perché no:

class MyException(Exception):
    pass

Ancora una volta, il problema con quanto sopra è che per prenderlo, dovresti nominarlo in modo specifico (importandolo se creato altrove) o catturare Eccezione (ma probabilmente non sei pronto a gestire tutti i tipi di Eccezioni, e dovresti rilevare solo le eccezioni che sei disposto a gestire). Critiche simili a quelle riportate di seguito, ma in più non è questo il modo di inizializzare tramite supere otterrai un DeprecationWarningaccesso se accedi all'attributo message:

Modifica: per sovrascrivere qualcosa (o passare argomenti extra), procedere come segue:

class ValidationError(Exception):
    def __init__(self, message, errors):

        # Call the base class constructor with the parameters it needs
        super(ValidationError, self).__init__(message)

        # Now for your custom code...
        self.errors = errors

In questo modo è possibile passare messaggi di errore al secondo parametro e accedervi successivamente con e.errors

Richiede inoltre di passare esattamente due argomenti (a parte il self.) Niente di più, niente di meno. Questo è un vincolo interessante che i futuri utenti potrebbero non apprezzare.

Ad essere diretti - viola la sostituibilità di Liskov .

Dimostrerò entrambi gli errori:

>>> ValidationError('foo', 'bar', 'baz').message

Traceback (most recent call last):
  File "<pyshell#10>", line 1, in <module>
    ValidationError('foo', 'bar', 'baz').message
TypeError: __init__() takes exactly 3 arguments (4 given)

>>> ValidationError('foo', 'bar').message
__main__:1: DeprecationWarning: BaseException.message has been deprecated as of Python 2.6
'foo'

Rispetto a:

>>> MyAppValueError('foo', 'FOO', 'bar').message
'foo'

2
Ciao dal 2018! BaseException.messageè andato in Python 3, quindi la critica vale solo per le vecchie versioni, giusto?
Kos,

5
@Kos La critica sulla sostituibilità di Liskov è ancora valida. Anche la semantica del primo argomento come "messaggio" è discutibilmente discutibile, ma non credo che discuterò il punto. Lo darò più di uno sguardo quando avrò più tempo libero.
Aaron Hall

1
FWIW, per Python 3 (almeno per versione 3.6 e successive), avrebbe ridefinire il __str__metodo MyAppValueErrorinvece di basarsi sul messageattributo
Jacquot

1
@AaronHall Potresti espandere i vantaggi della sottoclasse ValueError anziché di Exception? Affermate che questo è ciò che si intende con i documenti, ma una lettura diretta non supporta tale interpretazione e nel Tutorial di Python in Eccezioni definite dall'utente lo rende chiaramente la scelta degli utenti: "Le eccezioni dovrebbero in genere derivare dalla classe Exception, direttamente o indirettamente. " Quindi desideroso di capire se la tua opinione è giustificabile, per favore.
ostergaard,

1
@ostergaard Non riesco a rispondere per intero in questo momento, ma in breve, l'utente ha la possibilità aggiuntiva di catturare ValueError. Ciò ha senso se rientra nella categoria degli errori di valore. Se non rientra nella categoria degli errori di valore, discuterei contro di essa sulla semantica. C'è spazio per alcune sfumature e ragionamenti da parte del programmatore, ma preferisco di gran lunga la specificità quando applicabile. Aggiornerò la mia risposta per affrontare meglio l'argomento presto.
Aaron Hall

50

vedere come funzionano le eccezioni per impostazione predefinita se vengono utilizzati uno o più attributi (omessi traceback):

>>> raise Exception('bad thing happened')
Exception: bad thing happened

>>> raise Exception('bad thing happened', 'code is broken')
Exception: ('bad thing happened', 'code is broken')

quindi potresti voler avere una sorta di " modello di eccezione ", che funziona come un'eccezione stessa, in modo compatibile:

>>> nastyerr = NastyError('bad thing happened')
>>> raise nastyerr
NastyError: bad thing happened

>>> raise nastyerr()
NastyError: bad thing happened

>>> raise nastyerr('code is broken')
NastyError: ('bad thing happened', 'code is broken')

questo può essere fatto facilmente con questa sottoclasse

class ExceptionTemplate(Exception):
    def __call__(self, *args):
        return self.__class__(*(self.args + args))
# ...
class NastyError(ExceptionTemplate): pass

e se non ti piace quella rappresentazione predefinita simile a una tupla, aggiungi semplicemente il __str__metodo alla ExceptionTemplateclasse, come:

    # ...
    def __str__(self):
        return ': '.join(self.args)

e avrai

>>> raise nastyerr('code is broken')
NastyError: bad thing happened: code is broken

32

A partire da Python 3.8 (2018, https://docs.python.org/dev/whatsnew/3.8.html ), il metodo raccomandato è ancora:

class CustomExceptionName(Exception):
    """Exception raised when very uncommon things happen"""
    pass

Non dimenticare di documentare, perché è necessaria un'eccezione personalizzata!

Se necessario, questa è la strada da percorrere per le eccezioni con più dati:

class CustomExceptionName(Exception):
    """Still an exception raised when uncommon things happen"""
    def __init__(self, message, payload=None):
        self.message = message
        self.payload = payload # you could add more args
    def __str__(self):
        return str(self.message) # __str__() obviously expects a string to be returned, so make sure not to send any other data types

e recuperali come:

try:
    raise CustomExceptionName("Very bad mistake.", "Forgot upgrading from Python 1")
except CustomExceptionName as error:
    print(str(error)) # Very bad mistake
    print("Detail: {}".format(error.payload)) # Detail: Forgot upgrading from Python 1

payload=Noneè importante renderlo sottaceto. Prima di scaricarlo, devi chiamare error.__reduce__(). Il caricamento funzionerà come previsto.

Forse dovresti indagare per trovare una soluzione usando l' returnistruzione pythons se hai bisogno di trasferire molti dati su una struttura esterna. Questo sembra essere più chiaro / più pitonico per me. Le eccezioni avanzate sono ampiamente utilizzate in Java, che a volte può essere fastidioso quando si utilizza un framework e si devono rilevare tutti i possibili errori.


1
Per lo meno, i documenti attuali indicano che questo è il modo di farlo (almeno senza il __str__) piuttosto che altre risposte che usanosuper().__init__(...) .. Solo un peccato che sostituisce __str__e __repr__probabilmente è necessario solo per una migliore serializzazione "predefinita".
kevlarr,

2
Domanda onesta: perché è importante che le eccezioni siano in grado di essere sottaceto? Quali sono i casi d'uso per le eccezioni di dumping e caricamento?
Roel Schroeven,

1
@RoelSchroeven: ho dovuto parallelizzare il codice una volta. Ha funzionato con un singolo processo, ma alcuni aspetti di alcune delle sue classi non erano serializzabili (la funzione lambda veniva passata come oggetti). Mi ci è voluto del tempo per capirlo e risolverlo. Ciò significa che qualcuno in seguito potrebbe finire per aver bisogno che il codice sia serializzato, non riesca a farlo e debba scavare perché ... Il mio problema non era errori impercettibili, ma posso vederlo causare problemi simili.
logicOnAbstractions

17

Dovresti sovrascrivere __repr__o __unicode__metodi invece di usare message, gli argomenti che fornisci quando costruisci l'eccezione saranno argsnell'attributo dell'oggetto eccezione.


7

No, il "messaggio" non è proibito. È appena deprecato. L'applicazione funzionerà perfettamente con l'utilizzo del messaggio. Ma potresti voler eliminare l'errore di deprecazione, ovviamente.

Quando crei classi di eccezione personalizzate per la tua applicazione, molte di esse non effettuano la sottoclasse solo da Exception, ma da altre, come ValueError o simili. Quindi devi adattarti al loro uso delle variabili.

E se hai molte eccezioni nella tua applicazione, di solito è una buona idea avere una classe base personalizzata comune per tutti loro, in modo che gli utenti dei tuoi moduli possano fare

try:
    ...
except NelsonsExceptions:
    ...

E in quel caso puoi fare il __init__ and __str__ necessario lì, quindi non devi ripeterlo per ogni eccezione. Ma semplicemente chiamare la variabile del messaggio qualcos'altro rispetto al messaggio fa il trucco.

In ogni caso, hai solo bisogno __init__ or __str__di fare qualcosa di diverso da quello che fa l'eccezione stessa. E perché se la deprecazione, allora hai bisogno di entrambi, o ricevi un errore. Non è necessario un sacco di codice extra per classe. ;)


È interessante notare che le eccezioni di Django non ereditano da una base comune. docs.djangoproject.com/en/2.2/_modules/django/core/exceptions Hai un buon esempio quando è necessario rilevare tutte le eccezioni da un'applicazione specifica? (forse è utile solo per alcuni tipi specifici di applicazioni).
Yaroslav Nikitenko,

Ho trovato un buon articolo su questo argomento, julien.danjou.info/python-exceptions-guide . Penso che le eccezioni dovrebbero essere sottoclassate principalmente in base al dominio, non in base all'applicazione. Quando l'app riguarda il protocollo HTTP, si ottiene da HTTPError. Quando parte dell'app è TCP, derivate le eccezioni di quella parte da TCPError. Ma se la tua app si estende su molti domini (file, autorizzazioni, ecc.), Il motivo per avere MyBaseException diminuisce. O è per proteggere dalla "violazione dei livelli"?
Yaroslav Nikitenko,

6

Vedi un ottimo articolo " La guida definitiva alle eccezioni di Python ". I principi di base sono:

  • Eredita sempre da (almeno) Eccezione.
  • Chiama sempre BaseException.__init__con un solo argomento.
  • Quando si crea una libreria, definire una classe base che eredita da Exception.
  • Fornire dettagli sull'errore.
  • Eredita dai tipi di eccezioni incorporati quando ha senso.

Ci sono anche informazioni sull'organizzazione (in moduli) e sull'involucro delle eccezioni, consiglio di leggere la guida.


1
Questo è un buon esempio del perché su SO di solito controllo la risposta più votata, ma anche le più recenti. Inoltre utile, grazie.
logicOnAbstractions

1
Always call BaseException.__init__ with only one argument.Sembra un vincolo non necessario, dal momento che accetta effettivamente un numero qualsiasi di argomenti.
Eugene Yarmash,

@EugeneYarmash Sono d'accordo, ora non lo capisco. Non lo uso comunque. Forse dovrei rileggere l'articolo ed espandere la mia risposta.
Yaroslav Nikitenko,

@EugeneYarmash Ho letto di nuovo l'articolo. Si afferma che in caso di più argomenti l'implementazione C chiama "return PyObject_Str (self-> args);" Significa che una stringa dovrebbe funzionare meglio di molte altre. L'hai controllato?
Yaroslav Nikitenko,

3

Prova questo esempio

class InvalidInputError(Exception):
    def __init__(self, msg):
        self.msg = msg
    def __str__(self):
        return repr(self.msg)

inp = int(input("Enter a number between 1 to 10:"))
try:
    if type(inp) != int or inp not in list(range(1,11)):
        raise InvalidInputError
except InvalidInputError:
    print("Invalid input entered")

1

Per definire correttamente le tue eccezioni, ci sono alcune best practice che devi seguire:

  • Definire una classe base che eredita da Exception. Ciò consentirà di rilevare eventuali eccezioni correlate al progetto (eccezioni più specifiche dovrebbero ereditare da esso):

    class MyProjectError(Exception):
        """A base class for MyProject exceptions."""

    Organizzare queste classi di eccezione in un modulo separato (ad es. exceptions.py) È generalmente una buona idea.

  • Per creare un'eccezione personalizzata, eseguire la sottoclasse della classe di eccezioni di base.

  • Per aggiungere il supporto per argomenti aggiuntivi a un'eccezione personalizzata, definire un __init__()metodo personalizzato con un numero variabile di argomenti. Chiama la classe base __init__(), passando ad essa qualsiasi argomento posizionale (ricorda che BaseException/Exception aspetta un numero qualsiasi di argomenti posizionali ):

    class CustomError(MyProjectError):
        def __init__(self, *args, **kwargs):
            super(CustomError, self).__init__(*args)
            self.foo = kwargs.get('foo')

    Per sollevare tale eccezione con un argomento in più puoi usare:

    raise CustomError('Something bad happened', foo='foo')

Questo design aderisce al principio di sostituzione di Liskov , poiché è possibile sostituire un'istanza di una classe di eccezioni di base con un'istanza di una classe di eccezioni derivata. Inoltre, consente di creare un'istanza di una classe derivata con gli stessi parametri del genitore.

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.