Richtige Möglichkeit, benutzerdefinierte Ausnahmen in modernem Python zu deklarieren?

1290

Was ist der richtige Weg, um benutzerdefinierte Ausnahmeklassen in modernem Python zu deklarieren? Mein primäres Ziel ist es, den Standard anderer Ausnahmeklassen zu befolgen, damit (zum Beispiel) jede zusätzliche Zeichenfolge, die ich in die Ausnahme einbinde, von dem Tool ausgedruckt wird, das die Ausnahme abgefangen hat.

Mit "modernem Python" meine ich etwas, das in Python 2.5 ausgeführt wird, aber für Python 2.6 und Python 3. * korrekt ist. * Vorgehensweise. Und mit "benutzerdefiniert" meine ich ein Ausnahmeobjekt, das zusätzliche Daten zur Fehlerursache enthalten kann: eine Zeichenfolge, möglicherweise auch ein anderes für die Ausnahme relevantes beliebiges Objekt.

Ich wurde durch die folgende Verfallswarnung in Python 2.6.2 ausgelöst:

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

Es scheint verrückt, dass BaseExceptioneine besondere Bedeutung für die genannten Attribute hat message. Ich versammle mich aus PEP-352 dass dieses Attribut in 2.5 eine besondere Bedeutung hatte. Sie versuchen, es zu verwerfen. Ich denke also, dass dieser Name (und dieser allein) jetzt verboten ist. Pfui.

Ich bin mir auch unscharf bewusst, dass Exceptiones einige magische Parameter gibt args, aber ich habe nie gewusst, wie man sie benutzt. Ich bin mir auch nicht sicher, ob dies der richtige Weg ist, um die Dinge in Zukunft zu erledigen. Viele der Diskussionen, die ich online fand, deuteten darauf hin, dass sie versuchten, Argumente in Python 3 zu beseitigen.

Update: Zwei Antworten haben vorgeschlagen, überschrieben zu werden __init__, und __str__/ __unicode__/ __repr__. Das scheint viel zu tippen, ist es notwendig?

Nelson
quelle

Antworten:

1322

Vielleicht habe ich die Frage verpasst, aber warum nicht:

class MyException(Exception):
    pass

Bearbeiten: Um etwas zu überschreiben (oder zusätzliche Argumente zu übergeben), gehen Sie folgendermaßen vor:

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

Auf diese Weise können Sie Fehlermeldungen an den zweiten Parameter übergeben und später mit darauf zugreifen e.errors


Python 3-Update: In Python 3+ können Sie diese etwas kompaktere Verwendung verwenden von 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
gahooa
quelle
35
Eine so definierte Ausnahme wäre jedoch nicht auswählbar. siehe die Diskussion hier stackoverflow.com/questions/16244923/…
jiakai
86
@jiakai bedeutet "picklable". :-)
Robino
1
Nach der Dokumentation von Python für benutzerdefinierte Ausnahmen sind die in der Funktion __init__ genannten Namen falsch. Anstelle von (Selbst, Nachricht, Fehler) ist es (Selbst, Ausdruck, Nachricht). Der Attributausdruck ist der Eingabeausdruck, in dem der Fehler aufgetreten ist, und die Nachricht ist eine Erklärung des Fehlers.
ddleon
2
Das ist ein Missverständnis, @ddleon. Das Beispiel in den Dokumenten, auf die Sie sich beziehen, bezieht sich auf einen bestimmten Anwendungsfall. Der Name der Konstruktorargumente der Unterklasse (und ihre Anzahl) hat keine Bedeutung.
Asthasr
498

Mit moderner Python Ausnahmen, brauchen Sie nicht , um Missbrauch .messageoder Überschreibung .__str__()oder .__repr__()oder irgendetwas davon. Wenn Sie beim Auslösen Ihrer Ausnahme nur eine informative Nachricht wünschen, gehen Sie wie folgt vor:

class MyException(Exception):
    pass

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

Das gibt einen Traceback, der mit endet MyException: My hovercraft is full of eels.

Wenn Sie mehr Flexibilität für die Ausnahme wünschen, können Sie ein Wörterbuch als Argument übergeben:

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

Es ist jedoch exceptetwas komplizierter, diese Details in einem Block zu erfassen. Die Details werden im argsAttribut gespeichert , das eine Liste ist. Sie müssten so etwas tun:

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

Es ist weiterhin möglich, mehrere Elemente an die Ausnahme zu übergeben und über Tupelindizes darauf zuzugreifen. Dies wird jedoch dringend empfohlen (und war vor einiger Zeit sogar für die Verwerfung vorgesehen). Wenn Sie mehr als eine einzelne Information benötigen und die oben beschriebene Methode für Sie nicht ausreicht, sollten Sie Exceptiondie im Lernprogramm beschriebene Unterklasse verwenden .

class MyError(Exception):
    def __init__(self, message, animal):
        self.message = message
        self.animal = animal
    def __str__(self):
        return self.message
frnknstn
quelle
2
"aber das wird in Zukunft veraltet sein" - ist das noch veraltet? Python 3.7 scheint immer noch glücklich zu akzeptieren Exception(foo, bar, qux).
mtraceur
In jüngster Zeit wurden keine Arbeiten durchgeführt, um dies zu verhindern, da der letzte Versuch aufgrund der Schmerzen beim Übergang fehlgeschlagen ist. Von dieser Verwendung wird jedoch weiterhin abgeraten. Ich werde meine Antwort aktualisieren, um dies widerzuspiegeln.
Freitag,
@frnknstn, warum wird davon abgeraten? Sieht aus wie eine nette Redewendung für mich.
Neves
2
@neves für den Anfang: Die Verwendung von Tupeln zum Speichern von Ausnahmeinformationen hat keinen Vorteil gegenüber der Verwendung eines Wörterbuchs, um dasselbe zu tun. Wenn Sie an den Gründen
frnknstn
Der relevante Abschnitt von PEP352 ist "Zurückgezogene Ideen" .
Liberforce
196

"Richtige Möglichkeit, benutzerdefinierte Ausnahmen in modernem Python zu deklarieren?"

Dies ist in Ordnung, es sei denn, Ihre Ausnahme ist wirklich eine Art spezifischere Ausnahme:

class MyException(Exception):
    pass

Oder besser (vielleicht perfekt), anstatt passeinen Docstring zu geben:

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

Unterklassen Ausnahme Unterklassen

Aus den Dokumenten

Exception

Alle integrierten, nicht systemexitierenden Ausnahmen werden von dieser Klasse abgeleitet. Alle benutzerdefinierten Ausnahmen sollten ebenfalls von dieser Klasse abgeleitet werden.

Das heißt, wenn Ihre Ausnahme ein Typ einer spezifischeren Ausnahme ist, klassifizieren Sie diese Ausnahme anstelle der generischen Exception(und das Ergebnis ist, dass Sie immer noch davon ableiten, Exceptionwie in den Dokumenten empfohlen). Sie können auch mindestens eine Dokumentzeichenfolge angeben (und nicht gezwungen sein, das passSchlüsselwort zu verwenden):

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

Legen Sie Attribute fest, die Sie selbst mit einem benutzerdefinierten erstellen __init__. Vermeiden Sie es, ein Diktat als Positionsargument zu übergeben. Zukünftige Benutzer Ihres Codes werden es Ihnen danken. Wenn Sie das veraltete Nachrichtenattribut verwenden, wird durch die Zuweisung selbst Folgendes vermieden 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) 

Es ist wirklich nicht nötig, eigene __str__oder zu schreiben __repr__. Die eingebauten sind sehr nett und Ihre kooperative Vererbung stellt sicher, dass Sie sie verwenden.

Kritik der Top-Antwort

Vielleicht habe ich die Frage verpasst, aber warum nicht:

class MyException(Exception):
    pass

Wiederum besteht das Problem mit dem oben Gesagten darin, dass Sie es entweder spezifisch benennen müssen (importieren, wenn es an anderer Stelle erstellt wurde) oder Ausnahme abfangen müssen, um es abzufangen (aber Sie sind wahrscheinlich nicht bereit, alle Arten von Ausnahmen zu behandeln). und Sie sollten nur Ausnahmen abfangen, für die Sie bereit sind). Ähnliche Kritik wie unten, aber zusätzlich ist dies nicht die Möglichkeit, über zu initialisieren super, und Sie erhalten eine, DeprecationWarningwenn Sie auf das Nachrichtenattribut zugreifen:

Bearbeiten: Um etwas zu überschreiben (oder zusätzliche Argumente zu übergeben), gehen Sie folgendermaßen vor:

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

Auf diese Weise können Sie Fehlermeldungen an den zweiten Parameter übergeben und später mit e.errors darauf zugreifen

Außerdem müssen genau zwei Argumente (abgesehen von self) übergeben werden. Nicht mehr und nicht weniger. Dies ist eine interessante Einschränkung, die zukünftige Benutzer möglicherweise nicht zu schätzen wissen.

Direkt zu sein - es verletzt die Substituierbarkeit von Liskov .

Ich werde beide Fehler demonstrieren:

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

Verglichen mit:

>>> MyAppValueError('foo', 'FOO', 'bar').message
'foo'
Aaron Hall
quelle
2
Hallo ab 2018! BaseException.messageist in Python 3 weg, also gilt die Kritik nur für alte Versionen, oder?
Kos
5
@Kos Die Kritik an der Substituierbarkeit von Liskov ist weiterhin gültig. Die Semantik des ersten Arguments als "Nachricht" ist ebenfalls fraglich, aber ich glaube nicht, dass ich den Punkt argumentieren werde. Ich werde das genauer betrachten, wenn ich mehr Freizeit habe.
Aaron Hall
1
FWIW, für Python 3 (mindestens für 3.6+) würde man die __str__Methode neu definieren , MyAppValueErroranstatt sich auf das messageAttribut zu verlassen
Jacquot
1
@AaronHall Könnten Sie den Vorteil der Unterklassifizierung von ValueError anstelle von Exception erweitern? Sie geben an, dass dies mit den Dokumenten gemeint ist, aber ein direktes Lesen unterstützt diese Interpretation nicht. Im Python-Lernprogramm unter Benutzerdefinierte Ausnahmen ist dies eindeutig die Wahl des Benutzers: "Ausnahmen sollten normalerweise von der Ausnahmeklasse abgeleitet werden. entweder direkt oder indirekt. " Daher möchten Sie bitte verstehen, ob Ihre Ansicht gerechtfertigt ist.
Ostergaard
1
@ostergaard Kann momentan nicht vollständig antworten, aber kurz gesagt, der Benutzer erhält die zusätzliche Option zum Fangen ValueError. Dies ist sinnvoll, wenn es sich um die Kategorie der Wertfehler handelt. Wenn es nicht in der Kategorie der Wertfehler liegt, würde ich in der Semantik dagegen argumentieren. Es gibt Raum für einige Nuancen und Überlegungen seitens des Programmierers, aber ich bevorzuge die Spezifität, wenn zutreffend. Ich werde meine Antwort aktualisieren, um das Thema bald besser anzugehen.
Aaron Hall
50

sehen , wie Ausnahmen standardmäßig arbeiten , wenn man gegen mehrere Attribute verwendet werden (Tracebacks weggelassen):

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

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

Daher möchten Sie möglicherweise eine Art " Ausnahmevorlage " haben, die auf kompatible Weise als Ausnahme selbst fungiert:

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

Dies kann mit dieser Unterklasse problemlos durchgeführt werden

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

und wenn Ihnen diese standardmäßige tupelartige Darstellung nicht gefällt, fügen Sie __str__der ExceptionTemplateKlasse einfach eine Methode hinzu , wie:

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

und du wirst haben

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

Ab Python 3.8 (2018, https://docs.python.org/dev/whatsnew/3.8.html ) lautet die empfohlene Methode weiterhin:

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

Bitte vergessen Sie nicht zu dokumentieren, warum eine benutzerdefinierte Ausnahme erforderlich ist!

Wenn nötig, ist dies der richtige Weg, um Ausnahmen mit mehr Daten zu vermeiden:

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

und holen sie wie:

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=Noneist wichtig, damit es beizbar ist. Bevor Sie es ausgeben, müssen Sie anrufen error.__reduce__(). Das Laden funktioniert wie erwartet.

Möglicherweise sollten Sie bei der Suche nach einer Lösung mithilfe der Python- returnAnweisung nachforschen, ob viele Daten für die Übertragung in eine äußere Struktur erforderlich sind. Dies scheint mir klarer / pythonischer zu sein. Erweiterte Ausnahmen werden in Java häufig verwendet, was manchmal ärgerlich sein kann, wenn ein Framework verwendet wird und alle möglichen Fehler abgefangen werden müssen.

Fameman
quelle
1
Zumindest zeigen die aktuellen Dokumente, dass dies der Weg ist (zumindest ohne __str__) und nicht andere Antworten, die verwendet werden super().__init__(...). Nur eine Schande, die für eine bessere "Standard" -Serialisierung überschreibt __str__und __repr__wahrscheinlich notwendig ist.
Kevlarr
2
Ehrliche Frage: Warum ist es wichtig, dass Ausnahmen beizbar sind? Was sind die Anwendungsfälle für Dumping- und Ladeausnahmen?
Roel Schroeven
1
@RoelSchroeven: Ich musste Code einmal parallelisieren. Es lief ein guter Einzelprozess, aber Aspekte einiger seiner Klassen waren nicht serialisierbar (Lambda-Funktion wurde als Objekte übergeben). Ich habe einige Zeit gebraucht, um es herauszufinden und zu reparieren. Das bedeutet, dass jemand später Ihren Code möglicherweise serialisieren muss, nicht in der Lage ist, dies zu tun, und herausfinden muss, warum ... Mein Problem war kein nicht auswählbarer Fehler, aber ich kann sehen, dass er ähnliche Probleme verursacht.
LogicOnAbstractions
17

Sie sollten __repr__oder überschreiben __unicode__, anstatt die Nachricht zu verwenden. Die Argumente, die Sie beim Erstellen der Ausnahme angeben, befinden sich im argsAttribut des Ausnahmeobjekts.

M. Utku ALTINKAYA
quelle
7

Nein, "Nachricht" ist nicht verboten. Es ist nur veraltet. Ihre Anwendung funktioniert gut mit der Verwendung von Nachrichten. Aber vielleicht möchten Sie den Verfallsfehler natürlich beseitigen.

Wenn Sie benutzerdefinierte Ausnahmeklassen für Ihre Anwendung erstellen, werden viele von ihnen nicht nur von Exception, sondern auch von anderen wie ValueError oder ähnlichem unterklassifiziert. Dann müssen Sie sich an die Verwendung von Variablen anpassen.

Und wenn Ihre Anwendung viele Ausnahmen enthält, ist es normalerweise eine gute Idee, eine gemeinsame benutzerdefinierte Basisklasse für alle zu haben, damit Benutzer Ihrer Module dies tun können

try:
    ...
except NelsonsExceptions:
    ...

Und in diesem Fall können Sie das tun __init__ and __str__ Notwendige dort , sodass Sie es nicht für jede Ausnahme wiederholen müssen. Aber einfach die Nachrichtenvariable etwas anderes als Nachricht aufzurufen, reicht aus.

In jedem Fall benötigen Sie das nur, __init__ or __str__wenn Sie etwas anderes tun als Exception selbst. Und denn wenn die Abwertung, dann brauchen Sie beide, oder Sie bekommen eine Fehlermeldung. Das ist nicht viel zusätzlicher Code, den Sie pro Klasse benötigen. ;)

Lennart Regebro
quelle
Es ist interessant, dass Django-Ausnahmen nicht von einer gemeinsamen Basis erben. docs.djangoproject.com/de/2.2/_modules/django/core/exceptions Haben Sie ein gutes Beispiel, wenn alle Ausnahmen von einer bestimmten Anwendung abgefangen werden müssen? (Vielleicht ist es nur für bestimmte Arten von Anwendungen nützlich).
Jaroslaw Nikitenko
Ich habe einen guten Artikel zu diesem Thema gefunden, julien.danjou.info/python-exceptions-guide . Ich denke, dass Ausnahmen in erster Linie domänenbasiert und nicht anwendungsbasiert sein sollten. Wenn es in Ihrer App um das HTTP-Protokoll geht, leiten Sie es von HTTPError ab. Wenn ein Teil Ihrer App TCP ist, leiten Sie die Ausnahmen dieses Teils von TCPError ab. Wenn sich Ihre App jedoch über viele Domänen erstreckt (Datei, Berechtigungen usw.), verringert sich der Grund für eine MyBaseException. Oder soll es vor "Ebenenverletzungen" schützen?
Jaroslaw Nikitenko
6

Siehe einen sehr guten Artikel " Die endgültige Anleitung zu Python-Ausnahmen ". Die Grundprinzipien sind:

  • Erben Sie immer von (mindestens) Exception.
  • Rufen Sie immer BaseException.__init__mit nur einem Argument an.
  • Definieren Sie beim Erstellen einer Bibliothek eine Basisklasse, die von Exception erbt.
  • Geben Sie Details zum Fehler an.
  • Erben Sie von eingebauten Ausnahmetypen, wenn dies sinnvoll ist.

Es gibt auch Informationen zum Organisieren (in Modulen) und zum Umschließen von Ausnahmen. Ich empfehle, die Anleitung zu lesen.

Jaroslaw Nikitenko
quelle
1
Dies ist ein gutes Beispiel dafür, warum ich bei SO normalerweise die am besten bewertete Antwort überprüfe, aber auch die aktuellsten. Nützliche Ergänzung, danke.
LogicOnAbstractions
1
Always call BaseException.__init__ with only one argument.Scheint eine nicht benötigte Einschränkung zu sein, da tatsächlich eine beliebige Anzahl von Argumenten akzeptiert wird .
Eugene Yarmash
@ EugeneYarmash Ich stimme zu, jetzt verstehe ich das nicht. Ich benutze es sowieso nicht. Vielleicht sollte ich den Artikel noch einmal lesen und meine Antwort erweitern.
Jaroslaw Nikitenko
@EugeneYarmash Ich habe den Artikel noch einmal gelesen. Es wird angegeben, dass bei mehreren Argumenten die C-Implementierung "return PyObject_Str (self-> args)" aufruft. Dies bedeutet, dass eine Zeichenfolge besser funktionieren sollte als mehrere. Hast du das überprüft?
Jaroslaw Nikitenko
3

Versuchen Sie dieses Beispiel

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")
Omkaar.K
quelle
1

Um Ihre eigenen Ausnahmen richtig zu definieren, müssen Sie einige bewährte Methoden befolgen:

  • Definieren Sie eine Basisklasse, von der geerbt wird Exception. Auf diese Weise können Ausnahmen im Zusammenhang mit dem Projekt abgefangen werden (spezifischere Ausnahmen sollten davon erben):

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

    Das Organisieren dieser Ausnahmeklassen in einem separaten Modul (z. B. exceptions.py) ist im Allgemeinen eine gute Idee.

  • Unterklassifizieren Sie die Basisausnahmeklasse, um eine benutzerdefinierte Ausnahme zu erstellen.

  • Definieren Sie eine benutzerdefinierte __init__()Methode mit einer variablen Anzahl von Argumenten , um einer benutzerdefinierten Ausnahme zusätzliche Argumente hinzuzufügen . Rufen Sie die Basisklasse auf __init__()und übergeben Sie ihr Positionsargumente (denken Sie daran BaseException/Exception erwarten Sie eine beliebige Anzahl von Positionsargumenten ):

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

    Um eine solche Ausnahme mit einem zusätzlichen Argument auszulösen, können Sie Folgendes verwenden:

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

Dieses Design entspricht dem Liskov-Substitutionsprinzip , da Sie eine Instanz einer Basisausnahmeklasse durch eine Instanz einer abgeleiteten Ausnahmeklasse ersetzen können. Außerdem können Sie eine Instanz einer abgeleiteten Klasse mit denselben Parametern wie die übergeordnete Klasse erstellen.

Eugene Yarmash
quelle