Wann und wie soll ich Ausnahmen verwenden?

20

Die Einstellung

Ich habe oft Probleme festzustellen, wann und wie ich Ausnahmen verwenden soll. Betrachten wir ein einfaches Beispiel: Angenommen, ich schabe eine Webseite mit der Aufschrift " http://www.abevigoda.com/ ", um festzustellen, ob Abe Vigoda noch am Leben ist. Dazu müssen wir nur die Seite herunterladen und nach Zeiten suchen, in denen der Ausdruck "Abe Vigoda" erscheint. Wir geben den ersten Auftritt zurück, da dies Abes Status einschließt. Konzeptionell sieht es so aus:

def get_abe_status(url):
    # download the page
    page = download_page(url)

    # get all mentions of Abe Vigoda
    hits = page.find_all_mentions("Abe Vigoda")

    # parse the first hit for his status
    status = parse_abe_status(hits[0])

    # he's either alive or dead
    return status == "alive"

Wobei parse_abe_status(s)eine Zeichenkette der Form "Abe Vigoda ist etwas " annimmt und den Teil " etwas " zurückgibt .

Bevor Sie argumentieren, dass es viel bessere und robustere Möglichkeiten gibt, diese Seite nach Abes Status zu durchsuchen, denken Sie daran, dass dies nur ein einfaches und ausgeklügeltes Beispiel ist, das verwendet wird, um eine häufige Situation hervorzuheben, in der ich mich befinde.

Wo kann dieser Code auf Probleme stoßen? Einige "erwartete" Fehler sind unter anderem:

  • download_pageist möglicherweise nicht in der Lage, die Seite herunterzuladen, und wirft eine IOError.
  • Die URL verweist möglicherweise nicht auf die richtige Seite, oder die Seite wurde falsch heruntergeladen, sodass es keine Treffer gibt. hitsist also die leere Liste.
  • Die Webseite wurde geändert, wodurch möglicherweise unsere Vermutungen über die Seite falsch sind. Vielleicht erwarten wir 4 Erwähnungen von Abe Vigoda, aber jetzt finden wir 5.
  • Aus bestimmten Gründen ist hits[0]möglicherweise keine Zeichenfolge der Form "Abe Vigoda ist etwas " vorhanden und kann daher nicht korrekt analysiert werden.

Der erste Fall ist für mich eigentlich kein Problem: Ein IOErrorwird geworfen und kann vom Aufrufer meiner Funktion bearbeitet werden. Betrachten wir also die anderen Fälle und wie ich damit umgehen könnte. parse_abe_statusNehmen wir aber zunächst an, wir implementieren auf die dümmste Art und Weise:

def parse_abe_status(s):
    return s[13:]

Es wird nämlich keine Fehlerprüfung durchgeführt. Nun zu den Optionen:

Option 1: Rückkehr None

Ich kann dem Anrufer sagen, dass etwas schief gelaufen ist, indem ich zurückkehre None:

def get_abe_status(url):
    # download the page
    page = download_page(url)

    # get all mentions of Abe Vigoda
    hits = page.find_all_mentions("Abe Vigoda")

    if not hits:
        return None

    # parse the first hit for his status
    status = parse_abe_status(hits[0])

    # he's either alive or dead
    return status == "alive"

Wenn der Anrufer Nonevon meiner Funktion erhält , sollte er davon ausgehen, dass Abe Vigoda nicht erwähnt wurde, sodass ein Fehler aufgetreten ist. Aber das ist ziemlich vage, oder? Und es hilft nichts, wenn hits[0]es nicht so ist, wie wir es uns vorgestellt haben.

Auf der anderen Seite können wir einige Ausnahmen machen:

Option 2: Verwenden von Ausnahmen

Wenn hitsleer, IndexErrorwird beim Versuch ein geworfen hits[0]. Es ist jedoch nicht zu erwarten, dass der Anrufer eine IndexErrorvon meiner Funktion ausgelöste Aufgabe übernimmt, da er keine Ahnung hat, woher diese IndexErrorstammt. es hätte vorbei geworfen werden können find_all_mentions, soweit er weiß. Deshalb erstellen wir eine benutzerdefinierte Ausnahmeklasse, um dies zu handhaben:

class NotFoundError(Exception):
    """Throw this when something can't be found on a page."""

def get_abe_status(url):
    # download the page
    page = download_page(url)

    # get all mentions of Abe Vigoda
    hits = page.find_all_mentions("Abe Vigoda")

    try:
        hits[0]
    except IndexError:
        raise NotFoundError("No mentions found.")

    # parse the first hit for his status
    status = parse_abe_status(hits[0])

    # he's either alive or dead
    return status == "alive"

Was ist nun, wenn sich die Seite geändert hat und unerwartet viele Treffer zu verzeichnen sind? Dies ist keine Katastrophe, da der Code möglicherweise immer noch funktioniert, ein Anrufer jedoch besonders vorsichtig sein oder eine Warnung protokollieren möchte. Also werfe ich eine Warnung:

class NotFoundError(Exception):
    """Throw this when something can't be found on a page."""

def get_abe_status(url):
    # download the page
    page = download_page(url)

    # get all mentions of Abe Vigoda
    hits = page.find_all_mentions("Abe Vigoda")

    try:
        hits[0]
    except IndexError:
        raise NotFoundError("No mentions found.")

    # say we expect four hits...
    if len(hits) != 4:
        raise Warning("An unexpected number of hits.")
        logger.warning("An unexpected number of hits.")

    # parse the first hit for his status
    status = parse_abe_status(hits[0])

    # he's either alive or dead
    return status == "alive"

Schließlich stellen wir möglicherweise fest, dass statusdas weder lebend noch tot ist. Vielleicht stellte sich aus irgendeinem Grund heraus, dass es heute so war comatose. Dann möchte ich nicht zurückkehren False, da dies impliziert, dass Abe tot ist. Was soll ich hier machen? Wirf wahrscheinlich eine Ausnahme. Aber welche? Soll ich eine benutzerdefinierte Ausnahmeklasse erstellen?

class NotFoundError(Exception):
    """Throw this when something can't be found on a page."""

def get_abe_status(url):
    # download the page
    page = download_page(url)

    # get all mentions of Abe Vigoda
    hits = page.find_all_mentions("Abe Vigoda")

    try:
        hits[0]
    except IndexError:
        raise NotFoundError("No mentions found.")

    # say we expect four hits...
    if len(hits) != 4:
        raise Warning("An unexpected number of hits.")
        logger.warning("An unexpected number of hits.")

    # parse the first hit for his status
    status = parse_abe_status(hits[0])

    if status not in ['alive', 'dead']:
        raise SomeTypeOfError("Status is an unexpected value.")

    # he's either alive or dead
    return status == "alive"

Option 3: Irgendwo dazwischen

Ich denke, dass die zweite Methode mit Ausnahmen vorzuziehen ist, aber ich bin nicht sicher, ob ich Ausnahmen richtig darin verwende. Ich bin gespannt, wie erfahrene Programmierer damit umgehen würden.

jme
quelle

Antworten:

17

In Python wird empfohlen, Ausnahmen zu verwenden, um einen Fehler anzuzeigen. Dies gilt auch dann, wenn Sie regelmäßig mit einem Ausfall rechnen.

Betrachten Sie es aus der Perspektive des Aufrufers Ihres Codes:

my_status = get_abe_status(my_url)

Was ist, wenn wir None zurückgeben? Wenn der Aufrufer den Fall, dass get_abe_status fehlgeschlagen ist, nicht speziell behandelt, wird einfach versucht, mit my_stats als None fortzufahren. Dies kann später zu einem schwer zu diagnostizierenden Fehler führen. Auch wenn Sie auf None prüfen, hat dieser Code keine Ahnung, warum get_abe_status () fehlgeschlagen ist.

Aber was ist, wenn wir eine Ausnahme auslösen? Wenn der Aufrufer den Fall nicht speziell behandelt, wird die Ausnahme nach oben weitergeleitet und trifft schließlich auf den Standardausnahmebehandler. Das ist vielleicht nicht das, was Sie wollen, aber es ist besser, als einen subtilen Fehler an einer anderen Stelle im Programm einzuführen. Darüber hinaus gibt die Ausnahme Auskunft darüber, was schief gelaufen ist, was in der ersten Version verloren gegangen ist.

Aus Sicht des Aufrufers ist es einfach bequemer, eine Ausnahme als einen Rückgabewert abzurufen. Und das ist der Python-Stil, um mithilfe von Ausnahmen anzugeben, dass Fehlerbedingungen keine Werte zurückgeben.

Einige werden eine andere Perspektive einnehmen und argumentieren, dass Sie Ausnahmen nur für Fälle verwenden sollten, mit denen Sie nie wirklich rechnen. Sie argumentieren, dass normales Laufen keine Ausnahmen auslösen sollte. Ein Grund dafür ist, dass Ausnahmen äußerst ineffizient sind, aber das gilt eigentlich nicht für Python.

Ein paar Punkte in Ihrem Code:

try:
    hits[0]
except IndexError:
    raise NotFoundError("No mentions found.")

Das ist eine sehr verwirrende Möglichkeit, nach einer leeren Liste zu suchen. Induziere keine Ausnahme, nur um etwas zu überprüfen. Verwenden Sie ein if.

# say we expect four hits...
if len(hits) != 4:
    raise Warning("An unexpected number of hits.")
    logger.warning("An unexpected number of hits.")

Sie wissen, dass die logger.warning-Zeile nie richtig ausgeführt wird?

Winston Ewert
quelle
1
Vielen Dank (verspätet) für Ihre Antwort. Zusammen mit dem Blick auf veröffentlichten Code hat dies mein Gefühl verbessert, wann und wie ich eine Ausnahme auslösen kann.
jme
4

Die akzeptierte Antwort verdient es, akzeptiert zu werden und beantwortet die Frage. Ich schreibe dies nur, um ein bisschen zusätzlichen Hintergrund zu bieten.

Ein Credo von Python ist: Es ist einfacher, um Verzeihung zu bitten als um Erlaubnis. Dies bedeutet, dass Sie normalerweise nur Dinge tun und, wenn Sie Ausnahmen erwarten, diese behandeln. Im Gegensatz zu früheren Überprüfungen, um sicherzustellen, dass Sie keine Ausnahme erhalten.

Ich möchte Ihnen anhand eines Beispiels zeigen, wie dramatisch der Unterschied in der Mentalität von C ++ / Java ist. Eine for-Schleife in C ++ sieht normalerweise so aus:

for(int i = 0; i != myvector.size(); ++i) ...

Eine Möglichkeit, darüber nachzudenken: myvector[k]Wenn Sie auf where k> = myvector.size () zugreifen, wird eine Ausnahme ausgelöst. Man könnte das also im Prinzip (sehr umständlich) als Versuchsfang schreiben.

    for(int i = 0; ; ++i)  {
        try {
           ...
        } catch (& std::out_of_range)
             break

Oder etwas ähnliches. Überlegen Sie nun, was in einer Python-for-Schleife passiert:

for i in range(1):
    ...

Wie funktioniert das Die for-Schleife nimmt das Ergebnis von range (1) und ruft iter () auf, um einen Iterator dorthin zu holen.

b = range(1).__iter__()

Dann ruft es bei jeder Schleifeniteration das nächste Mal auf, bis ...:

>>> next(b)
0
>>> next(b)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

Mit anderen Worten, eine for-Schleife in Python ist eigentlich ein Versuch, außer in der Verkleidung.

Was die konkrete Frage betrifft, denken Sie daran, dass Ausnahmen die normale Funktionsausführung stoppen und separat behandelt werden müssen. In Python sollten Sie sie frei werfen, wenn es keinen Sinn macht, den Rest des Codes in Ihrer Funktion auszuführen, und / oder wenn keine der Rückgaben korrekt widerspiegelt, was in der Funktion passiert ist. Beachten Sie, dass eine frühzeitige Rückkehr von einer Funktion anders ist: Eine frühzeitige Rückkehr bedeutet, dass Sie die Antwort bereits herausgefunden haben und den Rest des Codes nicht benötigen, um die Antwort herauszufinden. Ich sage, dass Ausnahmen ausgelöst werden sollten, wenn die Antwort nicht bekannt ist und der Rest des Codes zur Bestimmung der Antwort nicht vernünftig ausgeführt werden kann. Jetzt ist es nur noch eine Frage der Dokumentation, sich selbst "richtig zu reflektieren", wie die Ausnahmen, die Sie auslösen.

Im Falle Ihres speziellen Codes würde ich sagen, dass jede Situation, die dazu führt, dass Treffer eine leere Liste sind, ausgelöst werden sollte. Warum? Nun, so wie Ihre Funktion eingerichtet ist, gibt es keine Möglichkeit, die Antwort zu bestimmen, ohne Treffer zu analysieren. Wenn also Treffer nicht syntaktisch analysiert werden können, entweder weil die URL schlecht ist oder weil Treffer leer sind, kann die Funktion die Frage nicht beantworten und es auch nicht wirklich versuchen.

In diesem speziellen Fall würde ich argumentieren, dass Sie auch dann noch werfen sollten, wenn es Ihnen gelingt, zu analysieren und keine vernünftige Antwort zu erhalten (lebend oder tot). Warum? Weil die Funktion einen Booleschen Wert zurückgibt. Die Rücksendung von None ist für Ihren Kunden sehr gefährlich. Wenn sie das Kontrollkästchen Keine aktivieren, tritt kein Fehler auf, sondern es wird nur stillschweigend als Falsch behandelt. Ihr Client muss also grundsätzlich immer eine Prüfung durchführen, wenn keine vorhanden ist, wenn er keine stillen Fehler wünscht. Sie sollten also wahrscheinlich nur werfen.

Nir Friedman
quelle
2

Sie sollten Ausnahmen verwenden, wenn etwas Außergewöhnliches auftritt. Das heißt, etwas, das bei ordnungsgemäßer Verwendung der Anwendung nicht auftreten sollte. Wenn es für den Verbraucher Ihrer Methode zulässig und zu erwarten ist, nach etwas zu suchen, das nicht gefunden wird, ist "nicht gefunden" kein Ausnahmefall. In diesem Fall sollten Sie null oder "None" oder {} oder etwas zurückgeben, das auf eine leere Rückgabemenge hinweist.

Wenn Sie andererseits wirklich erwarten, dass die Konsumenten Ihrer Methode immer das finden, wonach gesucht wird (es sei denn, sie haben es irgendwie vermasselt), dann ist es eine Ausnahme, dies nicht zu finden, und Sie sollten dies tun.

Der Schlüssel ist, dass die Ausnahmebehandlung teuer sein kann - Ausnahmen sollen Informationen über den Zustand Ihrer Anwendung sammeln, wenn sie auftreten, z. Ich glaube nicht, dass Sie das versuchen.

Matthew Flynn
quelle
1
Wenn Sie der Meinung sind, dass es nicht zulässig ist, einen Wert zu finden, sollten Sie vorsichtig sein, um anzuzeigen, dass dies der Fall war. Wenn Ihre Methode a zurückgeben soll Stringund Sie "None" als Indikator auswählen, bedeutet dies, dass Sie darauf achten müssen, dass "None" niemals ein gültiger Wert ist. Beachten Sie außerdem, dass es einen Unterschied gibt, ob Sie die Daten anzeigen und keinen Wert finden und die Daten nicht abrufen können. Daher können wir die Daten nicht finden. Das gleiche Ergebnis für diese beiden Fälle zu haben, bedeutet, dass Sie keine Sichtbarkeit haben, wenn Sie keinen Wert erhalten, wenn Sie erwarten, dass es einen gibt.
Unholysampler
Inline-Codeblöcke sind mit einem Backtick (`) markiert. Vielleicht wollten Sie das mit dem" None "machen?
Izkata
3
Ich fürchte, das ist in Python absolut falsch. Sie wenden C ++ / Java-Argumentation auf eine andere Sprache an. Python verwendet Ausnahmen, um das Ende einer for-Schleife anzugeben. das ist ziemlich ungewöhnlich.
Nir Friedman
2

Wenn ich eine Funktion schreibe

 def abe_is_alive():

Ich würde es an return Trueoder Falsein den Fällen schreiben, in denen ich mir der einen oder anderen absolut sicher bin, und raisein jedem anderen Fall einen Fehler (zB raise ValueError("Status neither 'dead' nor 'alive'")). Dies liegt daran, dass die Funktion, die mine aufruft, einen Booleschen Wert erwartet, und wenn ich dies nicht mit Sicherheit angeben kann, sollte der reguläre Programmfluss nicht fortgesetzt werden.

So etwas wie Ihr Beispiel, bei dem Sie eine andere Anzahl von "Treffern" erhalten als erwartet, würde ich wahrscheinlich ignorieren. Solange einer der Treffer noch zu meinem Muster "Abe Vigoda ist {dead | alive}" passt, ist das in Ordnung. Auf diese Weise kann die Seite neu angeordnet werden, erhält jedoch weiterhin die entsprechenden Informationen.

Eher, als

try:
    hits[0] 
except IndexError:
    raise NotFoundError

Ich würde ausdrücklich prüfen:

if not hits:
    raise NotFoundError

da dies tendenziell "billiger" ist als das aufstellen der try.

Ich stimme dir zu IOError; Ich würde auch nicht versuchen, Fehler beim Herstellen einer Verbindung mit der Website zu machen. Wenn dies aus irgendeinem Grund nicht möglich ist, ist dies nicht der geeignete Ort, um die Verbindung herzustellen (da dies uns nicht bei der Beantwortung unserer Frage hilft) auf die aufrufende Funktion.

jonrsharpe
quelle