Gibt es Ausnahmen für bewährte Methoden zur Flusskontrolle in Python?

15

Ich lese "Learning Python" und bin auf Folgendes gestoßen:

Benutzerdefinierte Ausnahmen können auch Fehlerzustände anzeigen. Beispielsweise kann eine Suchroutine codiert werden, um eine Ausnahme auszulösen, wenn eine Übereinstimmung gefunden wird, anstatt ein Statusflag zurückzugeben, das der Anrufer interpretieren soll. Im Folgenden erledigt der try / except / else-Exception-Handler die Arbeit eines if / else-Rückgabewert-Testers:

class Found(Exception): pass

def searcher():
    if ...success...:
        raise Found()            # Raise exceptions instead of returning flags
    else:
        return

Da Python dynamisch typisiert und für den Kern polymorph ist, sind Ausnahmen anstelle von Sentinel-Rückgabewerten die im Allgemeinen bevorzugte Methode, um solche Bedingungen zu signalisieren.

Ich habe so etwas schon mehrmals in verschiedenen Foren und bei Verweisen auf Python mit StopIteration zum Beenden von Schleifen gesehen, kann aber nicht viel in den offiziellen Styleguides finden. oder Aussagen von Entwicklern. Gibt es irgendetwas Offizielles, das besagt, dass dies die beste Vorgehensweise für Python ist?

Dies ( Werden Ausnahmen als Kontrollfluss als schwerwiegendes Antimuster angesehen? Wenn ja, warum? ) auch mehrere Kommentatoren besagt, dass dieser Stil Pythonic ist. Worauf basiert das?

TIA

JB
quelle

Antworten:

24

Der allgemeine Konsens "keine Ausnahmen verwenden!" Kommt meist aus anderen Sprachen und ist auch dort manchmal veraltet.

  • In C ++ ist das Auslösen einer Ausnahme aufgrund der Stapelabwicklung sehr kostspielig . Jede lokale Variablendeklaration ähnelt einer withAnweisung in Python, und das Objekt in dieser Variablen kann Destruktoren ausführen. Diese Destruktoren werden ausgeführt, wenn eine Ausnahme ausgelöst wird, aber auch, wenn von einer Funktion zurückgekehrt wird. Dieses „RAII-Idiom“ ist ein integrales Sprachfeature und für das Schreiben von robustem, korrektem Code überaus wichtig. Daher war es ein Kompromiss zwischen RAII und billigen Ausnahmen, den C ++ für RAII entschied.

  • In früheren Versionen von C ++ wurde viel Code nicht ausnahmesicher geschrieben: Wenn Sie RAII nicht tatsächlich verwenden, können Speicher und andere Ressourcen leicht verloren gehen. Wenn Sie also Ausnahmen auslösen, wird der Code falsch dargestellt. Dies ist nicht mehr sinnvoll, da selbst in der C ++ - Standardbibliothek Ausnahmen verwendet werden: Sie können nicht so tun, als gäbe es keine Ausnahmen. Bei der Kombination von C-Code mit C ++ treten jedoch weiterhin Ausnahmen auf.

  • In Java ist jeder Ausnahme ein Stack-Trace zugeordnet. Der Stack-Trace ist beim Debuggen von Fehlern sehr hilfreich, aber es ist mühsam, wenn die Ausnahme nie gedruckt wird, z. B. weil sie nur für den Kontrollfluss verwendet wurde.

In diesen Sprachen sind Ausnahmen also „zu teuer“, um als Kontrollfluss verwendet zu werden. In Python ist dies weniger ein Problem und Ausnahmen sind viel billiger. Darüber hinaus leidet die Python-Sprache bereits unter einem gewissen Aufwand, der die Kosten für Ausnahmen im Vergleich zu anderen Kontrollflusskonstrukten unbemerkt lässt: Beispielsweise ist die Überprüfung, ob ein Diktateintrag mit einem expliziten Mitgliedschaftstest vorhanden if key in the_dict: ...ist, im Allgemeinen genauso schnell wie der bloße Zugriff auf den Eintrag the_dict[key]; ...und die Überprüfung, ob Sie Holen Sie sich einen KeyError. Einige integrale Sprachfunktionen (z. B. Generatoren) sind in Ausnahmefällen ausgelegt.

Obwohl es keinen technischen Grund gibt, Ausnahmen in Python gezielt zu vermeiden, besteht immer noch die Frage, ob Sie sie anstelle von Rückgabewerten verwenden sollten. Die Probleme auf Designebene mit Ausnahmen sind:

  • Sie sind überhaupt nicht offensichtlich. Sie können sich eine Funktion nicht leicht ansehen und sehen, welche Ausnahmen sie auslösen kann, sodass Sie nicht immer wissen, was zu fangen ist. Der Rückgabewert ist in der Regel genauer definiert.

  • Ausnahmen sind nicht-lokale Kontrollabläufe, die Ihren Code komplizieren. Wenn Sie eine Ausnahme auslösen, wissen Sie nicht, wo der Kontrollfluss fortgesetzt wird. Bei Fehlern, die nicht sofort behoben werden können, ist dies wahrscheinlich eine gute Idee. Wenn Sie Ihren Anrufer über eine Bedingung informieren, ist dies völlig unnötig.

Die Python-Kultur neigt im Allgemeinen zu Ausnahmen, aber es ist leicht, über Bord zu gehen. Stellen Sie sich eine list_contains(the_list, item)Funktion vor, die prüft, ob die Liste ein Element enthält, das diesem Element entspricht. Wenn das Ergebnis über Ausnahmen kommuniziert wird, ist das absolut ärgerlich, weil wir es so nennen müssen:

try:
  list_contains(invited_guests, person_at_door)
except Found:
  print("Oh, hello {}!".format(person_at_door))
except NotFound:
  print("Who are you?")

Ein Bool zurückzugeben wäre viel klarer:

if list_contains(invited_guests, person_at_door):
  print("Oh, hello {}!".format(person_at_door))
else:
  print("Who are you?")

Wenn die Funktion bereits einen Wert zurückgeben soll, ist die Rückgabe eines speziellen Werts für spezielle Bedingungen eher fehleranfällig, da die Benutzer vergessen, diesen Wert zu überprüfen (dies ist wahrscheinlich die Ursache für 1/3 der Probleme in C). Eine Ausnahme ist normalerweise korrekter.

Ein gutes Beispiel ist eine pos = find_string(haystack, needle)Funktion, die nach dem ersten Auftreten von suchtneedle Strings im Heuhaufen-String und die Startposition zurückgibt. Aber was ist, wenn die Heuhaufen-Schnur nicht die Nadelschnur enthält?

Die von C und Python imitierte Lösung besteht darin, einen speziellen Wert zurückzugeben. In C ist dies ein Nullzeiger, in Python ist dies -1. Dies führt zu überraschenden Ergebnissen, wenn die Position als String-Index ohne Prüfung verwendet wird, insbesondere als-1 es sich um einen gültigen Index in Python handelt. In C gibt Ihnen Ihr NULL-Zeiger mindestens einen Segfault.

In PHP wird ein spezieller Wert eines anderen Typs zurückgegeben: der Boolesche Wert FALSEanstelle einer Ganzzahl. Wie sich herausstellt, ist dies aufgrund der impliziten Konvertierungsregeln der Sprache nicht besser (beachten Sie jedoch, dass in Python auch Boolesche Werte als Ints verwendet werden können!). Funktionen, die keinen konsistenten Typ zurückgeben, werden im Allgemeinen als sehr verwirrend angesehen.

Eine robustere Variante wäre gewesen, eine Ausnahme auszulösen, wenn die Zeichenfolge nicht gefunden werden kann. Dadurch wird sichergestellt, dass es im normalen Kontrollfluss unmöglich ist, den Spezialwert versehentlich anstelle eines normalen Werts zu verwenden:

 try:
   pos = find_string(haystack, needle)
   do_something_with(pos)
 except NotFound:
   ...

Alternativ kann immer ein Typ zurückgegeben werden, der nicht direkt verwendet werden kann, sondern zuerst entpackt werden muss, z. B. ein result-bool-Tupel, bei dem der Boolesche Wert angibt, ob eine Ausnahme aufgetreten ist oder ob das Ergebnis verwendet werden kann. Dann:

pos, ok = find_string(haystack, needle)
if not ok:
  ...
do_something_with(pos)

Dies zwingt Sie, Probleme sofort zu behandeln, aber es wird sehr schnell ärgerlich. Es verhindert auch, dass Sie leicht verketten können. Jeder Funktionsaufruf benötigt jetzt drei Codezeilen. Golang ist eine Sprache, die denkt, dass dieses Ärgernis die Sicherheit wert ist.

Ausnahmen sind also nicht ganz unproblematisch und können definitiv überbeansprucht werden, insbesondere wenn sie einen „normalen“ Rückgabewert ersetzen. Wenn Sie jedoch spezielle Bedingungen signalisieren (nicht unbedingt nur Fehler), können Sie mithilfe von Ausnahmen APIs entwickeln, die sauber, intuitiv, benutzerfreundlich und schwer zu missbrauchen sind.

amon
quelle
1
Vielen Dank für die ausführliche Antwort. Ich komme aus anderen Sprachen wie Java, in denen "Ausnahmen nur für Ausnahmebedingungen gelten". Das ist für mich also neu. Gibt es Python-Kernentwickler, die dies jemals so ausgedrückt haben wie in anderen Python-Richtlinien wie EAFP, EIBTI usw. (auf einer Mailing-Liste, einem Blog-Post usw.)? fragt ein anderer Entwickler / Chef. Vielen Dank!
JB
Indem Sie die fillInStackTrace-Methode Ihrer benutzerdefinierten Ausnahmeklasse überladen, um nur sofort zurückzukehren, können Sie das Auslösen sehr kostengünstig gestalten, da das Stack-Walking teuer ist und hier ausgeführt wird.
John Cowan
Ihre erste Kugel in Ihrem Intro war zunächst verwirrend. Vielleicht könnten Sie es in so etwas wie "Ausnahmebehandlungscode in modernem C ++ ist kostenlos, aber das Auslösen einer Ausnahme ist teuer" ändern ? (für Lauer : stackoverflow.com/questions/13835817/… ) [bearbeiten: vorgeschlagen bearbeiten]
phresnel
Beachten Sie, dass für Wörterbuch-Suchvorgänge mit einem collections.defaultdictoder my_dict.get(key, default)der Code viel klarer alstry: my_dict[key] except: return default
Steve Barnes
11

NEIN! - nicht generell - Ausnahmen gelten mit Ausnahme einer einzelnen Klasse von Code nicht als gute Flusssteuerungspraxis. Die einzige Stelle, an der Ausnahmen als vernünftige oder sogar bessere Möglichkeit zur Signalisierung eines Zustands angesehen werden, sind Generator- oder Iteratoroperationen. Diese Operationen können jeden möglichen Wert als gültiges Ergebnis zurückgeben, sodass ein Mechanismus zum Signalisieren eines Endes erforderlich ist.

Überlegen Sie, ob Sie eine Binärdatei mit einem Stream von einem Byte nach dem anderen lesen möchten - absolut jeder Wert ist ein potenziell gültiges Ergebnis, aber wir müssen immer noch ein Dateiende signalisieren. Wir haben also die Wahl, jedes Mal zwei Werte (den Bytewert und ein gültiges Flag) zurückzugeben oder eine Ausnahme auszulösen, wenn nichts mehr zu tun ist. In beiden Fällen kann der konsumierende Code folgendermaßen aussehen:

# Using validity flag
valid, val = readbyte(source)
while valid:
    processbyte(val)
    valid, val = readbyte(source)
tidy_up()

Alternative:

# With exceptions
try:
   val = readbyte(source)
   processbyte(val) # Note if a problem occurs here it will also raise an exception
except Exception: # Use a specific exception here!
   tidy_up()

Aber dies wurde, seit PEP 343 implementiert und zurückportiert wurde, alles ordentlich in der withAnweisung zusammengefasst. Das Obige wird zur sehr pythonischen:

with open(source) as input:
    for val in input.readbyte(): # This line will raise a StopIteration exception an call input.__exit__()
        processbyte(val) # Not called if there is nothing read

In python3 wurde dies:

for val in open(source, 'rb').read():
     processbyte(val)

Ich fordere Sie nachdrücklich auf, PEP 343 zu lesen, das den Hintergrund, die Gründe, Beispiele usw. enthält.

Es ist auch üblich, eine Ausnahme zu verwenden, um das Ende der Verarbeitung anzuzeigen, wenn Generatorfunktionen zum Anzeigen des Endes verwendet werden.

Ich möchte hinzufügen, dass Ihr Suchbeispiel mit ziemlicher Sicherheit rückwärts ist. Solche Funktionen sollten Generatoren sein, die beim ersten Aufruf die erste Übereinstimmung zurückgeben, dann Substituentenaufrufe, die die nächste Übereinstimmung zurückgeben, und eine NotFoundAusnahme auslösen, wenn es keine weiteren Übereinstimmungen gibt.

Steve Barnes
quelle
Sie sagen: "NEIN! - nicht generell - Ausnahmen werden mit Ausnahme einer einzelnen Codeklasse nicht als gute Ablaufsteuerungspraxis angesehen." Wo ist die Begründung für diese nachdrückliche Aussage? Diese Antwort scheint sich eng auf den Iterationsfall zu konzentrieren. Es gibt viele andere Fälle, in denen die Leute denken könnten, Ausnahmen zu verwenden. Was ist das Problem damit in diesen anderen Fällen?
Daniel Waltrip
@DanielWaltrip Ich sehe viel zu viel Code in verschiedenen Programmiersprachen, wo Ausnahmen verwendet werden, oft viel zu weit, wenn eine einfache ifbesser wäre. Wenn es um das Debuggen und Testen von Codes oder gar das Begründen des Verhaltens Ihres Codes geht, ist es besser, sich nicht auf Ausnahmen zu verlassen - sie sollten die Ausnahme sein. Ich habe im Produktionscode eine Berechnung gesehen, die über einen Throw / Catch und nicht über einen einfachen Return zurückgegeben wurde. Dies bedeutete, dass bei einer Division durch Null ein zufälliger Wert zurückgegeben wurde und nicht ein Absturz auftrat, bei dem der Fehler mehrere Stunden lang auftrat Debuggen.
Steve Barnes
Hey @SteveBarnes, das Beispiel für das Teilen durch Null klingt nach einer schlechten Programmierung (mit einem zu allgemeinen Fang, der die Ausnahme nicht erneut auslöst).
wTyeRogers
Ihre Antwort scheint zu lauten: A) "NEIN!" B) Es gibt gültige Beispiele, bei denen der Kontrollfluss über Ausnahmen besser funktioniert als bei Alternativen. C) Eine Korrektur des Fragencodes, um Generatoren zu verwenden (die Ausnahmen als Kontrollfluss verwenden und dies auch tun). Obwohl Ihre Antwort im Allgemeinen informativ ist, scheinen darin keine Argumente für Punkt A zu existieren, für die ich, da sie fett gedruckt und scheinbar die wichtigste Aussage sind, Unterstützung erwarten würde. Wenn es unterstützt würde, würde ich Ihre Antwort nicht ablehnen.
Leider