Professionelle Fehlerbehandlung mit Python

In diesem Lernprogramm erfahren Sie, wie Sie mit Fehlerzuständen in Python aus Sicht des Gesamtsystems umgehen. Die Fehlerbehandlung ist ein kritischer Aspekt des Designs und wird von den niedrigsten Ebenen (manchmal der Hardware) bis hin zum Endbenutzer durchdrungen. Wenn Sie keine konsistente Strategie haben, ist Ihr System unzuverlässig, die Benutzererfahrung ist schlecht und Sie haben viele Probleme beim Debuggen und bei der Fehlerbehebung. 

Der Schlüssel zum Erfolg besteht darin, sich all dieser ineinandergreifenden Aspekte bewusst zu sein, sie explizit zu berücksichtigen und eine Lösung zu finden, die jeden Punkt anspricht.

Statuscodes vs. Ausnahmen

Es gibt zwei Hauptmodelle für die Fehlerbehandlung: Statuscodes und Ausnahmen. Statuscodes können von jeder Programmiersprache verwendet werden. Ausnahmen erfordern Sprach- / Laufzeitunterstützung. 

Python unterstützt Ausnahmen. Python und seine Standardbibliothek verwenden Ausnahmen großzügig, um über viele Ausnahmesituationen zu berichten, wie z. B. E / A-Fehler, Division durch Nullen, Indizierung außerhalb der Grenzen und auch einige Ausnahmesituationen wie das Ende der Iteration (obwohl sie verborgen sind). Die meisten Bibliotheken folgen und machen Ausnahmen.

Das bedeutet, dass Ihr Code die Ausnahmen, die von Python und Bibliotheken hervorgerufen werden, ohnehin behandeln muss. Daher können Sie Ausnahmebedingungen aus Ihrem Code auslösen, wenn dies erforderlich ist, und sich nicht auf Statuscodes verlassen.

Schnelles Beispiel

Bevor wir in das innere Heiligtum von Python-Ausnahmen eintauchen und Best Practices für die Fehlerbehandlung anwenden, sollten wir uns einige Ausnahmebehandlungen in Aktion ansehen:

def f (): return 4/0 def g (): Exception erhöhen ("Rufen Sie uns nicht an. Wir rufen Sie an") def h (): try: f () außer Exception als e: print (e) try: g () mit Ausnahme der Ausnahme als e: print (e)

Hier ist die Ausgabe beim Aufruf h ():

h () Division durch Null Rufen Sie uns nicht an. Wir werden dich anrufen

Python-Ausnahmen

Python-Ausnahmen sind Objekte, die in einer Klassenhierarchie organisiert sind. 

Hier ist die ganze Hierarchie:

BaseException + - SystemExit + - KeyboardInterrupt + - GeneratorExit + - Exception + - StopIteration + - StandardError | + - BufferError | + - ArithmeticError | | + - FloatingPointError | | + - OverflowError | | + - ZeroDivisionError | + - AssertionError | + - AttributeError | + - EnvironmentError | | + - IOError | | + - OSError | | + - WindowsError (Windows) | | + - VMSError (VMS) | + - EOFError | + - ImportError | + - LookupError | | + - IndexError | | + - KeyError | + - MemoryError | + - NameFehler | | + - UnboundLocalError | + - ReferenceError | + - RuntimeError | | + - NotImplementedError | + - SyntaxError | | + - Einrückungsfehler | | + - TabError | + - Systemfehler | + - TypeError | + - ValueError | + - UnicodeError | + - UnicodeDecodeError | + - UnicodeEncodeError | + - UnicodeTranslateError + - Warnung + - DeprecationWarning + - PendingDeprecationWarning + - LaufzeitWarning + - SyntaxWarning + - UserWarning + - FutureWarning + - ImportWarning + - UnicodeWarning + - BytesWarning  

Es gibt einige spezielle Ausnahmen, die direkt von abgeleitet werden BaseException, mögen SystemExit, KeyboardInterrupt und GeneratorExit. Dann ist da noch der Ausnahme Klasse, die Basisklasse für StopIteration, Standart Fehler und Warnung. Alle Standardfehler werden von abgeleitet Standart Fehler.

Wenn Sie eine Ausnahme auslösen oder eine von Ihnen aufgerufene Funktion eine Ausnahme auslöst, wird der normale Codefluss abgebrochen und die Ausnahme beginnt den Aufrufstapel weiterzuverfolgen, bis ein richtiger Ausnahmebehandler gefunden wird. Wenn keine Ausnahmebehandlungsroutine verfügbar ist, wird der Prozess (oder genauer der aktuelle Thread) mit einer nicht behandelten Ausnahmemeldung beendet.

Ausnahmen aufwerfen

Ausnahmen zu erhöhen ist sehr einfach. Sie benutzen einfach die erziehen Schlüsselwort, um ein Objekt zu erzeugen, das eine Unterklasse von ist Ausnahme Klasse. Es könnte ein Beispiel von sein Ausnahme selbst eine der Standardausnahmen (z. Laufzeit Fehler) oder eine Unterklasse von Ausnahme du hast dich selbst abgeleitet Hier ist ein kleiner Ausschnitt, der alle Fälle veranschaulicht:

# Eine Instanz der Exception-Klasse auslösen. Exception auslösen ('Ummm ... Etwas ist falsch') # Eine Instanz der RuntimeError-Klasse auslösen RuntimeError auslösen ('Ummm ... Etwas ist falsch') # Eine benutzerdefinierte Unterklasse der Exception auslösen, die den Zeitstempel enthält Die Ausnahme wurde aus der datetime-Klasse erstellt. datetime-Klasse SuperError (Ausnahme): def __init __ (self, message): Ausnahme .__ init __ (message) self.when = datetime.now () raise SuperError ('Ummm… irgendwas stimmt nicht')

Ausnahmen fangen

Sie fangen Ausnahmen mit der außer Klausel, wie Sie im Beispiel gesehen haben. Wenn Sie eine Ausnahme abfangen, haben Sie drei Optionen:

  • Leise schlucken (handhaben und weiterlaufen).
  • Führen Sie so etwas wie Protokollierung aus, aber wiederholen Sie dieselbe Ausnahme, um höhere Ebenen verarbeiten zu können.
  • Erzeuge eine andere Ausnahme als das Original.

Schlucke die Ausnahme

Sie sollten die Ausnahme schlucken, wenn Sie wissen, wie sie damit umgehen soll und vollständig wiederhergestellt wird. 

Wenn Sie beispielsweise eine Eingabedatei mit unterschiedlichen Formaten (JSON, YAML) erhalten, können Sie versuchen, diese mit anderen Parsern zu analysieren. Wenn der JSON-Parser eine Ausnahme ausgelöst hat, dass die Datei keine gültige JSON-Datei ist, schlucken Sie sie und versuchen es mit dem YAML-Parser. Wenn der YAML-Parser ebenfalls fehlgeschlagen ist, lassen Sie die Ausnahme aus.

import json import yaml def parse_file (dateiname): try: return json.load (open (Dateiname)) außer json.JSONDecodeError return yaml.load (open (Dateiname))

Beachten Sie, dass andere Ausnahmen (z. B. Datei nicht gefunden oder keine Leseberechtigungen) propagiert werden und von der spezifischen Ausnahmebedingung nicht erfasst werden. Dies ist eine gute Richtlinie in diesem Fall, wenn Sie die YAML-Analyse nur versuchen möchten, wenn die JSON-Analyse aufgrund eines JSON-Codierungsproblems fehlgeschlagen ist. 

Wenn du damit umgehen willst alles Ausnahmen dann einfach verwenden Ausnahme Ausnahme. Zum Beispiel:

def print_exception_type (func, * args, ** kwargs): try: return func (* args, ** kwargs) außer Ausnahme als e: print type (e)

Beachten Sie das, indem Sie hinzufügen wie e, Sie binden das Ausnahmeobjekt an den Namen e in Ihrer außer Klausel verfügbar.

Erhöhen Sie dieselbe Ausnahme erneut

Zum erneuten Anheben einfach hinzufügen erziehen ohne Argumente in Ihrem Handler. Auf diese Weise können Sie einige lokale Aktionen ausführen, aber auch die höheren Ebenen können damit umgehen. Hier die invoke_function () Die Funktion druckt den Typ der Ausnahme auf die Konsole und löst dann die Ausnahme erneut aus.

def invoke_function (func, * args, ** kwargs): try: return func (* args, ** kwargs) außer Ausnahme als e: print type (e) erhöhen

Eine andere Ausnahme auslösen

Es gibt mehrere Fälle, in denen Sie eine andere Ausnahme auslösen möchten. Manchmal möchten Sie mehrere untergeordnete Ausnahmen in einer einzigen Kategorie zusammenfassen, die einheitlich von übergeordnetem Code behandelt wird. In Auftragsfällen müssen Sie die Ausnahme in die Benutzerebene transformieren und einen anwendungsspezifischen Kontext bereitstellen. 

Endlich Klausel

Manchmal möchten Sie sicherstellen, dass Bereinigungscode ausgeführt wird, auch wenn irgendwo auf dem Weg eine Ausnahme ausgelöst wurde. Beispielsweise haben Sie möglicherweise eine Datenbankverbindung, die Sie schließen möchten, wenn Sie fertig sind. Hier ist der falsche Weg:

def fetch_some_data (): db = open_db_connection () Abfrage (db) close_db_Connection (db)

Wenn die Abfrage() Funktion löst eine Ausnahme aus, dann den Aufruf an close_db_connection () wird niemals ausgeführt und die DB-Verbindung bleibt geöffnet. Das endlich Die Klausel wird immer ausgeführt, nachdem ein try alle Exception-Handler ausgeführt wurde. So machen Sie es richtig:

def fetch_some_data (): db = Keiner try: db = open_db_connection () query (db) abschließend: wenn db kein None ist: close_db_connection (db)

Der Anruf an open_db_connection () kann keine Verbindung zurückgeben oder selbst eine Ausnahme auslösen. In diesem Fall muss die DB-Verbindung nicht geschlossen werden.

Beim Benutzen endlich, Sie müssen darauf achten, keine Ausnahmen zu melden, da diese die ursprüngliche Ausnahme verdecken.

Kontext-Manager

Kontextmanager bieten einen weiteren Mechanismus zum Einschließen von Ressourcen wie Dateien oder DB-Verbindungen in Bereinigungscode, der automatisch ausgeführt wird, auch wenn Ausnahmen ausgelöst wurden. Anstelle von try-finally-Blöcken verwenden Sie die mit Aussage. Hier ist ein Beispiel mit einer Datei:

def Prozessdatei (Dateiname): mit offen (Dateiname) als f: Prozess (f.read ()) 

Jetzt auch wenn verarbeiten() eine Ausnahme ausgelöst hat, wird die Datei sofort ordnungsgemäß geschlossen, wenn der Gültigkeitsbereich der mit Block wird beendet, unabhängig davon, ob die Ausnahme behandelt wurde oder nicht.

Protokollierung

Die Protokollierung ist in nicht-trivialen, lang laufenden Systemen eine ziemlich wichtige Anforderung. Dies ist besonders nützlich bei Webanwendungen, bei denen Sie alle Ausnahmen auf eine allgemeine Weise behandeln können: Protokollieren Sie einfach die Ausnahme und senden Sie eine Fehlermeldung an den Anrufer zurück. 

Bei der Protokollierung ist es hilfreich, den Ausnahmetyp, die Fehlernachricht und den Stacktrace zu protokollieren. Alle diese Informationen sind über die sys.exc_info Objekt, aber wenn Sie das verwenden logger.exception () Wenn Sie diese Methode in Ihrem Exception-Handler verwenden, extrahiert das Python-Protokollierungssystem alle relevanten Informationen für Sie.

Dies ist die bewährte Methode, die ich empfehle:

import logging logger = logging.getLogger () def f (): try: flaky_func () mit Ausnahme der Ausnahme: logger.exception () raise

Wenn Sie diesem Muster folgen (vorausgesetzt, Sie haben die Protokollierung korrekt eingerichtet), wird in allen Protokollen ein ziemlich guter Bericht darüber angezeigt, was schiefgegangen ist, und Sie können das Problem beheben.

Wenn Sie erneut erhöhen, stellen Sie sicher, dass Sie dieselbe Ausnahme nicht immer wieder auf verschiedenen Ebenen protokollieren. Dies ist eine Verschwendung und kann Sie verwirren und Sie denken, dass mehrere Instanzen desselben Problems aufgetreten sind, wenn in der Praxis eine einzelne Instanz mehrmals protokolliert wurde.

Der einfachste Weg, dies zu tun, besteht darin, alle Ausnahmen ausbreiten zu lassen (es sei denn, sie können sicher behandelt und früher verschluckt werden) und führen dann die Protokollierung in der Nähe der obersten Ebene Ihrer Anwendung / Ihres Systems durch.

Wache

Protokollierung ist eine Fähigkeit. Die am häufigsten verwendete Implementierung verwendet Protokolldateien. Für große verteilte Systeme mit Hunderten, Tausenden oder mehr Servern ist dies jedoch nicht immer die beste Lösung. 

Um die Ausnahmen in Ihrer gesamten Infrastruktur nachverfolgen zu können, ist ein Dienst wie der Wachposten sehr hilfreich. Es zentralisiert alle Ausnahmereports und fügt zusätzlich zum Stacktrace den Status jedes Stack-Frames hinzu (den Wert der Variablen zu dem Zeitpunkt, zu dem die Exception ausgelöst wurde). Es bietet auch eine sehr schöne Benutzeroberfläche mit Dashboards, Berichten und Methoden zum Aufteilen der Nachrichten in mehreren Projekten. Es handelt sich um Open Source, sodass Sie einen eigenen Server betreiben oder die gehostete Version abonnieren können.

Umgang mit vorübergehenden Versagen

Einige Ausfälle sind vorübergehend, insbesondere im Umgang mit verteilten Systemen. Ein System, das bei den ersten Anzeichen von Problemen ausflippt, ist nicht sehr nützlich. 

Wenn Ihr Code auf ein Remote-System zugreift, das nicht reagiert, ist die herkömmliche Lösung Timeouts, aber manchmal ist nicht für jedes System ein Timeout vorgesehen. Timeouts sind nicht immer einfach zu kalibrieren, wenn sich die Bedingungen ändern. 

Ein anderer Ansatz besteht darin, schnell auszufallen und es erneut zu versuchen. Der Vorteil ist, dass, wenn das Ziel schnell reagiert, Sie nicht viel Zeit im Schlaf verbringen müssen und sofort reagieren können. Wenn dies jedoch fehlgeschlagen ist, können Sie den Vorgang mehrmals wiederholen, bis Sie entscheiden, dass er wirklich nicht erreichbar ist, und eine Ausnahme auslösen. Im nächsten Abschnitt werde ich einen Dekorateur vorstellen, der dies für Sie tun kann.

Hilfreiche Dekorateure

Zwei Dekorateure, die bei der Fehlerbehandlung helfen können, sind die @log_error, die eine Ausnahme protokolliert und dann erneut auslöst, und die @wiederholen decorator, der wiederholt den Aufruf einer Funktion wiederholt.

Fehlerlogger

Hier ist eine einfache Implementierung. Der Dekorateur schließt ein Loggerobjekt aus. Wenn eine Funktion dekoriert wird und die Funktion aufgerufen wird, wird der Aufruf in eine try-except-Klausel eingeschlossen. Wenn es eine Ausnahme gab, wird sie protokolliert und schließlich die Ausnahme erneut ausgelöst.

def log_error (logger) def decor (f): @ functools.wraps (f) def verpackt (* args, ** kwargs): try: return f (* args, ** kwargs) außer Ausnahme als e: if logger: logger .Exception (e) Erhöhen Sie die Rückgabe verziert

So verwenden Sie es:

import logging logger = logging.getLogger () @log_error (logger) def f (): Exception auslösen ("Ich bin außergewöhnlich")

Retrier

Hier ist eine sehr gute Implementierung des @retry-Dekorators.

import time import math # Dekorator erneut versuchen mit exponentiellem Backoff-Def-Wiederholversuch (Versuche, Verzögerung = 3, Backoff = 2): "Wiederholt eine Funktion oder Methode, bis True zurückgegeben wird Die Verzögerung sollte sich nach jedem Fehler verlängern. Der Backoff-Wert muss größer als 1 sein, andernfalls handelt es sich nicht wirklich um einen Backoff-Wert. Die Versuche müssen mindestens 0 und die Verzögerung größer als 0 sein <= 1: raise ValueError("backoff must be greater than 1") tries = math.floor(tries) if tries < 0: raise ValueError("tries must be 0 or greater") if delay <= 0: raise ValueError("delay must be greater than 0") def deco_retry(f): def f_retry(*args, **kwargs): mtries, mdelay = tries, delay # make mutable rv = f(*args, **kwargs) # first attempt while mtries > 0: Wenn rv wahr ist: # Done bei Erfolg return return Mtries - = 1 # verbrauchen einen Versuch time.sleep (mdelay) # wait… mdelay * = backoff # zukünftig länger warten rv = f (* args, ** kwargs) # Versuchen Sie es erneut. Zurück False # Keine Versuche mehr :-( return f_retry # true decorator -> dekorierte Funktion return deco_retry # @retry (arg [,…]) -> true decorator

Fazit

Die Fehlerbehandlung ist sowohl für Benutzer als auch für Entwickler von entscheidender Bedeutung. Python bietet großartige Unterstützung in der Sprach- und Standardbibliothek für die Fehlerbehandlung auf Ausnahmebasis. Wenn Sie die bewährten Methoden gewissenhaft befolgen, können Sie diesen oft vernachlässigten Aspekt überwinden.

Lerne Python

Lernen Sie Python mit unserem kompletten Python-Tutorial, egal ob Sie gerade erst anfangen oder ein erfahrener Programmierer sind, der neue Fähigkeiten erlernen möchte.