it-swarm.com.de

Sind Ausnahmen für die Best Practice zur Flusskontrolle in Python?

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

Benutzerdefinierte Ausnahmen können auch fehlerfreie Bedingungen 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 kann. Im Folgenden führt der Ausnahmebehandler try/exception/else die Arbeit eines if/else-Rückgabewert-Testers aus:

class Found(Exception): pass

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

Da Python ist dynamisch typisiert und zum Kern polymorph), sind Ausnahmen anstelle von Sentinel-Rückgabewerten die allgemein bevorzugte Methode, um solche Bedingungen zu signalisieren.

Ich habe solche Dinge mehrmals in verschiedenen Foren und Verweisen auf Python mit StopIteration zum Beenden von Schleifen) gesehen, aber ich kann nicht viel in den offiziellen Styleguides finden (PEP 8 hat Ein Hinweis auf Ausnahmen für die Flusskontrolle) 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 ernstes Antimuster angesehen? Wenn ja, warum? ) hat auch mehrere Kommentatoren, die angeben, dass dieser Stil pythonisch ist. Worauf basiert das?

TIA

17
J B

Der allgemeine Konsens "Verwenden Sie keine Ausnahmen!" kommt meistens aus anderen Sprachen und selbst dort ist es manchmal veraltet.

  • In C++ ist das Auslösen einer Ausnahme sehr kostspielig aufgrund des "Abwickelns des Stapels". Jede lokale Variablendeklaration ähnelt einer with - Anweisung 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 Sprachmerkmal und sehr wichtig, um robusten, korrekten Code zu schreiben. Daher war RAII gegenüber billigen Ausnahmen ein Kompromiss, den C++ gegenüber RAII beschlossen hat.

  • In frühem C++ wurde viel Code nicht ausnahmesicher geschrieben: Wenn Sie RAII nicht tatsächlich verwenden, können leicht Speicher und andere Ressourcen verloren gehen. Das Auslösen von Ausnahmen würde diesen Code also falsch machen. Dies ist nicht mehr sinnvoll, da selbst die C++ - Standardbibliothek Ausnahmen verwendet: Sie können nicht so tun, als gäbe es keine Ausnahmen. Ausnahmen sind jedoch immer noch ein Problem beim Kombinieren von C-Code mit C++.

  • In Java ist jeder Ausnahme eine Stapelverfolgung zugeordnet. Die Stapelverfolgung ist beim Debuggen von Fehlern sehr wertvoll, verschwendet jedoch Mühe, wenn die Ausnahme nie gedruckt wird, z. weil es nur für den Kontrollfluss verwendet wurde.

In diesen Sprachen sind Ausnahmen „zu teuer“, um als Kontrollfluss verwendet zu werden. In Python ist dies weniger ein Problem und Ausnahmen sind viel billiger. Außerdem leidet die Sprache Python bereits unter einem gewissen Overhead, der die Kosten für Ausnahmen unbemerkt lässt im Vergleich zu anderen Kontrollflusskonstrukten: zB die Überprüfung, ob ein Diktateintrag mit einem expliziten Mitgliedschaftstest if key in the_dict: ... vorhanden ist, ist im Allgemeinen genauso schnell wie der einfache Zugriff auf den Eintrag the_dict[key]; ... und die Überprüfung, ob Sie einen KeyError erhalten Integrale Sprachmerkmale (z. B. Generatoren) sind in Bezug auf Ausnahmen ausgelegt.

Obwohl es keinen technischen Grund gibt, Ausnahmen in Python speziell zu vermeiden, stellt sich dennoch 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 eine Funktion nicht einfach betrachten und sehen, welche Ausnahmen sie auslösen kann, sodass Sie nicht immer wissen, was Sie abfangen müssen. Der Rückgabewert ist tendenziell 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 ist im Allgemeinen zugunsten von Ausnahmen geneigt, 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 ziemlich 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 der Zeichenfolge needle in der Heuhaufenzeichenfolge sucht und die Startposition zurückgibt. Aber was ist, wenn die Heuhaufenschnur die Nadelschnur nicht enthält?

Die Lösung von C und nachgeahmt von Python 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 ohne Überprüfung als Zeichenfolgenindex verwendet wird, insbesondere da -1 Ein gültiger Index in Python ist. In C gibt Ihnen Ihr NULL-Zeiger zumindest einen Segfault.

In PHP wird ein spezieller Wert eines anderen Typs zurückgegeben: der boolesche Wert FALSE anstelle 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 dies nicht tun Rückgabe eines konsistenten Typs wird im Allgemeinen als sehr verwirrend angesehen.

Eine robustere Variante wäre gewesen, eine Ausnahme auszulösen, wenn die Zeichenfolge nicht gefunden werden kann, wodurch sichergestellt wird, dass während des normalen Kontrollflusses der Sonderwert nicht versehentlich anstelle eines normalen Werts verwendet werden kann:

 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 ausgepackt werden muss, z. ein Ergebnis-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 dazu, Probleme sofort zu lösen, 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 glaubt, dass dieses Ärgernis die Sicherheit wert ist.

Zusammenfassend lässt sich sagen, dass Ausnahmen nicht ganz problemlos sind und definitiv überbeansprucht werden können, insbesondere wenn sie einen „normalen“ Rückgabewert ersetzen. Wenn Sie jedoch spezielle Bedingungen signalisieren (nicht unbedingt nur Fehler), können Ausnahmen Ihnen helfen, APIs zu entwickeln, die sauber, intuitiv, benutzerfreundlich und schwer zu missbrauchen sind.

29
amon

NEIN! - nicht allgemein - Ausnahmen gelten mit Ausnahme einer einzelnen Codeklasse nicht als gute Flusskontrollpraxis . Der einzige Ort, an dem Ausnahmen als vernünftige oder sogar bessere Möglichkeit zur Signalisierung einer Bedingung angesehen werden, sind Generator- oder Iteratoroperationen. Diese Operationen können jeden möglichen Wert als gültiges Ergebnis zurückgeben, sodass ein Mechanismus erforderlich ist, um ein Ende zu signalisieren.

Erwägen Sie, eine Binärdatei des Streams byteweise zu lesen - absolut jeder Wert ist ein potenziell gültiges Ergebnis, aber wir müssen immer noch ein Dateiende signalisieren. Wir haben also die Wahl, zwei Werte zurückzugeben (den Bytewert und ein gültiges Flag), jedes Mal, wenn oder eine Ausnahme auslösen, wenn es keine mehr gibt tun. 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()

Dies wurde jedoch, seit PEP 34 implementiert und zurückportiert wurde, ordentlich in die Anweisung with eingepackt. Das Obige wird zum 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 dringend auf, PEP 34 zu lesen, das den Hintergrund, die Begründung, Beispiele usw. enthält.

Es ist auch üblich, eine Ausnahme zu verwenden, um das Ende der Verarbeitung zu signalisieren, wenn Generatorfunktionen verwendet werden, um das Ende zu signalisieren.

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

11
Steve Barnes