German (Deutsch) translation by Władysław Łucyszyn (you can also view the original English article)
In diesem Tutorial erfahren Sie, wie Sie mit Fehlerbedingungen in Python aus Sicht des gesamten Systems umgehen. Die Fehlerbehandlung ist ein kritischer Aspekt des Designs und reicht von den niedrigsten Ebenen (manchmal der Hardware) bis zu den Endbenutzern. 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 liegt 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 zur 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 wie E/A-Fehler, dividiert durch Null, Indizierung außerhalb der Grenzen und einige nicht so außergewöhnliche Situationen wie das Ende der Iteration (obwohl sie ausgeblendet sind) zu berichten. Die meisten Bibliotheken folgen diesem Beispiel und erheben Ausnahmen.
Das bedeutet, dass Ihr Code ohnehin die von Python und Bibliotheken ausgelösten Ausnahmen verarbeiten muss. Sie können also bei Bedarf auch Ausnahmen von Ihrem Code auslösen und sich nicht auf Statuscodes verlassen.
Kurzes Beispiel
Bevor wir uns mit dem inneren Heiligtum der Python-Ausnahmen und den Best Practices für die Fehlerbehandlung befassen, sehen wir uns einige Ausnahmebehandlungen in Aktion an:
1 |
def f(): |
2 |
|
3 |
return 4 / 0 |
4 |
|
5 |
|
6 |
|
7 |
def g(): |
8 |
|
9 |
raise Exception("Don't call us. We'll call you") |
10 |
|
11 |
|
12 |
|
13 |
def h(): |
14 |
|
15 |
try: |
16 |
|
17 |
f() |
18 |
|
19 |
except Exception as e: |
20 |
|
21 |
print(e) |
22 |
|
23 |
try: |
24 |
|
25 |
g() |
26 |
|
27 |
except Exception as e: |
28 |
|
29 |
print(e) |
Hier ist die Ausgabe beim Aufruf von h():
1 |
h()
|
2 |
|
3 |
division by zero |
4 |
|
5 |
Don't call us. We'll call you
|
Python-Ausnahmen
Python-Ausnahmen sind Objekte, die in einer Klassenhierarchie organisiert sind.
Hier ist die gesamte Hierarchie:
1 |
BaseException |
2 |
|
3 |
+-- SystemExit |
4 |
|
5 |
+-- KeyboardInterrupt |
6 |
|
7 |
+-- GeneratorExit |
8 |
|
9 |
+-- Exception |
10 |
|
11 |
+-- StopIteration |
12 |
|
13 |
+-- StandardError |
14 |
|
15 |
| +-- BufferError |
16 |
|
17 |
| +-- ArithmeticError |
18 |
|
19 |
| | +-- FloatingPointError |
20 |
|
21 |
| | +-- OverflowError |
22 |
|
23 |
| | +-- ZeroDivisionError |
24 |
|
25 |
| +-- AssertionError |
26 |
|
27 |
| +-- AttributeError |
28 |
|
29 |
| +-- EnvironmentError |
30 |
|
31 |
| | +-- IOError |
32 |
|
33 |
| | +-- OSError |
34 |
|
35 |
| | +-- WindowsError (Windows) |
36 |
|
37 |
| | +-- VMSError (VMS) |
38 |
|
39 |
| +-- EOFError |
40 |
|
41 |
| +-- ImportError |
42 |
|
43 |
| +-- LookupError |
44 |
|
45 |
| | +-- IndexError |
46 |
|
47 |
| | +-- KeyError |
48 |
|
49 |
| +-- MemoryError |
50 |
|
51 |
| +-- NameError |
52 |
|
53 |
| | +-- UnboundLocalError |
54 |
|
55 |
| +-- ReferenceError |
56 |
|
57 |
| +-- RuntimeError |
58 |
|
59 |
| | +-- NotImplementedError |
60 |
|
61 |
| +-- SyntaxError |
62 |
|
63 |
| | +-- IndentationError |
64 |
|
65 |
| | +-- TabError |
66 |
|
67 |
| +-- SystemError |
68 |
|
69 |
| +-- TypeError |
70 |
|
71 |
| +-- ValueError |
72 |
|
73 |
| +-- UnicodeError |
74 |
|
75 |
| +-- UnicodeDecodeError |
76 |
|
77 |
| +-- UnicodeEncodeError |
78 |
|
79 |
| +-- UnicodeTranslateError |
80 |
|
81 |
+-- Warning |
82 |
|
83 |
+-- DeprecationWarning |
84 |
|
85 |
+-- PendingDeprecationWarning |
86 |
|
87 |
+-- RuntimeWarning |
88 |
|
89 |
+-- SyntaxWarning |
90 |
|
91 |
+-- UserWarning |
92 |
|
93 |
+-- FutureWarning |
94 |
|
95 |
+-- ImportWarning |
96 |
|
97 |
+-- UnicodeWarning |
98 |
|
99 |
+-- BytesWarning |
100 |
Es gibt einige spezielle Ausnahmen, die direkt von BaseException abgeleitet sind, z. B. SystemExit, KeyboardInterrupt und GeneratorExit. Dann gibt es die Exception-Klasse, die die Basisklasse für StopIteration, StandardError und Warning ist. Alle Standardfehler werden von StandardError abgeleitet.
Wenn Sie eine Ausnahme oder eine von Ihnen aufgerufene Funktion auslösen, wird dieser normale Codefluss beendet und die Ausnahme beginnt, den Aufrufstapel weiterzuleiten, bis ein geeigneter Ausnahmebehandler gefunden wird. Wenn kein Ausnahmebehandler verfügbar ist, wird der Prozess (oder genauer der aktuelle Thread) mit einer nicht behandelten Ausnahmemeldung beendet.
Ausnahmen auslösen
Ausnahmen zu machen ist sehr einfach. Sie verwenden einfach das Schlüsselwort raise, um ein Objekt auszulösen, das eine Unterklasse der Exception-Klasse ist. Dies kann eine Instanz von Exception selbst sein, eine der Standardausnahmen (z. B. RuntimeError) oder eine von Ihnen selbst abgeleitete Unterklasse von Exception. Hier ist ein kleiner Ausschnitt, der alle Fälle demonstriert:
1 |
# Raise an instance of the Exception class itself
|
2 |
|
3 |
raise Exception('Ummm... something is wrong') |
4 |
|
5 |
|
6 |
|
7 |
# Raise an instance of the RuntimeError class
|
8 |
|
9 |
raise RuntimeError('Ummm... something is wrong') |
10 |
|
11 |
|
12 |
|
13 |
# Raise a custom subclass of Exception that keeps the timestamp the exception was created
|
14 |
|
15 |
from datetime import datetime |
16 |
|
17 |
|
18 |
|
19 |
class SuperError(Exception): |
20 |
|
21 |
def __init__(self, message): |
22 |
|
23 |
Exception.__init__(message) |
24 |
|
25 |
self.when = datetime.now() |
26 |
|
27 |
|
28 |
|
29 |
|
30 |
|
31 |
raise SuperError('Ummm... something is wrong') |
Ausnahmen fangen
Sie fangen Ausnahmen mit der except klausel ab, wie Sie im Beispiel gesehen haben. Wenn Sie eine Ausnahme abfangen, haben Sie drei Möglichkeiten:
- Schlucken Sie es leise (handhaben Sie es und rennen Sie weiter).
- Führen Sie beispielsweise eine Protokollierung durch, aber lösen Sie dieselbe Ausnahme erneut aus, damit höhere Ebenen verarbeitet werden können.
- Auslösen einer anderen Ausnahme anstelle des Originals.
Schluck die Ausnahme
Sie sollten die Ausnahme schlucken, wenn Sie wissen, wie Sie damit umgehen sollen und sich vollständig erholen können.
Wenn Sie beispielsweise eine Eingabedatei erhalten, die möglicherweise in verschiedenen Formaten (JSON, YAML) vorliegt, können Sie versuchen, sie mit verschiedenen 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 Sie es mit dem YAML-Parser. Wenn der YAML-Parser ebenfalls fehlgeschlagen ist, lassen Sie die Ausnahme weitergeben.
1 |
import json |
2 |
|
3 |
import yaml |
4 |
|
5 |
|
6 |
|
7 |
def parse_file(filename): |
8 |
|
9 |
try: |
10 |
|
11 |
return json.load(open(filename)) |
12 |
|
13 |
except json.JSONDecodeError |
14 |
|
15 |
return yaml.load(open(filename)) |
Beachten Sie, dass andere Ausnahmen (z. B. Datei nicht gefunden oder keine Leseberechtigungen) weitergegeben werden und nicht von der spezifischen Ausnahmeklausel erfasst werden. Dies ist in diesem Fall eine gute Richtlinie, wenn Sie die YAML-Analyse nur dann versuchen möchten, wenn die JSON-Analyse aufgrund eines JSON-Codierungsproblems fehlgeschlagen ist.
Wenn Sie alle Ausnahmen behandeln möchten, verwenden Sie einfach except Exception. Beispielsweise:
1 |
def print_exception_type(func, *args, **kwargs): |
2 |
|
3 |
try: |
4 |
|
5 |
return func(*args, **kwargs) |
6 |
|
7 |
except Exception as e: |
8 |
|
9 |
print type(e) |
Beachten Sie, dass Sie durch Hinzufügen as e das Ausnahmeobjekt an den Namen e binden, der in Ihrer Ausnahmeklausel verfügbar ist.
Erhöhen Sie dieselbe Ausnahme erneut
Um erneut zu erhöhen, fügen Sie einfach eine raise ohne Argumente in Ihrem Handler hinzu. Auf diese Weise können Sie eine lokale Behandlung durchführen, aber auch die oberen Ebenen können damit umgehen. Hier druckt die Funktion invoke_function() den Ausnahmetyp an die Konsole und löst die Ausnahme erneut aus.
1 |
def invoke_function(func, *args, **kwargs): |
2 |
|
3 |
try: |
4 |
|
5 |
return func(*args, **kwargs) |
6 |
|
7 |
except Exception as e: |
8 |
|
9 |
print type(e) |
10 |
|
11 |
raise
|
Erhöhen Sie eine andere Ausnahme
Es gibt mehrere Fälle, in denen Sie eine andere Ausnahme auslösen möchten. Manchmal möchten Sie mehrere verschiedene Ausnahmen auf niedriger Ebene in einer einzigen Kategorie zusammenfassen, die von Code auf höherer Ebene einheitlich behandelt wird. In Auftragsfällen müssen Sie die Ausnahme auf Benutzerebene umwandeln 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, sobald Sie fertig sind. Hier ist der falsche Weg:
1 |
def fetch_some_data(): |
2 |
|
3 |
db = open_db_connection() |
4 |
|
5 |
query(db) |
6 |
|
7 |
close_db_Connection(db) |
Wenn die Funktion query() eine Ausnahme auslöst, wird der Aufruf von close_db_connection() niemals ausgeführt und die DB-Verbindung bleibt offen. Die finally-Klausel wird immer ausgeführt, nachdem ein try all exception-Handler ausgeführt wurde. So geht's richtig:
1 |
def fetch_some_data(): |
2 |
|
3 |
db = None |
4 |
|
5 |
try: |
6 |
|
7 |
db = open_db_connection() |
8 |
|
9 |
query(db) |
10 |
|
11 |
finally: |
12 |
|
13 |
if db is not None: |
14 |
|
15 |
close_db_connection(db) |
Der Aufruf von open_db_connection() gibt möglicherweise keine Verbindung zurück oder löst selbst eine Ausnahme aus. In diesem Fall muss die DB-Verbindung nicht geschlossen werden.
Bei der finally Verwendung müssen Sie darauf achten, dort keine Ausnahmen auszulösen, da diese die ursprüngliche Ausnahme maskieren.
Kontextmanager
Kontextmanager bieten einen weiteren Mechanismus zum Umschließen von Ressourcen wie Dateien oder DB-Verbindungen in Bereinigungscode, der automatisch ausgeführt wird, selbst wenn Ausnahmen ausgelöst wurden. Anstelle von try-finally-Blöcken verwenden Sie die with-Anweisung. Hier ist ein Beispiel mit einer Datei:
1 |
def process_file(filename): |
2 |
|
3 |
with open(filename) as f: |
4 |
|
5 |
process(f.read()) |
Selbst wenn process() eine Ausnahme ausgelöst hat, wird die Datei sofort ordnungsgemäß geschlossen, wenn der Bereich des with-Blocks beendet wird, unabhängig davon, ob die Ausnahme behandelt wurde oder nicht.
Protokollierung
Die Protokollierung ist in nicht trivialen, lang laufenden Systemen so ziemlich eine Voraussetzung. Dies ist besonders nützlich in Webanwendungen, in denen Sie alle Ausnahmen generisch behandeln können: Protokollieren Sie einfach die Ausnahme und senden Sie eine Fehlermeldung an den Anrufer.
Bei der Protokollierung ist es hilfreich, den Ausnahmetyp, die Fehlermeldung und die Stapelverfolgung zu protokollieren. Alle diese Informationen sind über das Objekt sys.exc_info verfügbar. Wenn Sie jedoch die Methode logger.exception() in Ihrem Ausnahmehandler verwenden, extrahiert das Python-Protokollierungssystem alle relevanten Informationen für Sie.
Dies ist die beste Vorgehensweise, die ich empfehle:
1 |
import logging |
2 |
|
3 |
logger = logging.getLogger() |
4 |
|
5 |
|
6 |
|
7 |
def f(): |
8 |
|
9 |
try: |
10 |
|
11 |
flaky_func() |
12 |
|
13 |
except Exception: |
14 |
|
15 |
logger.exception() |
16 |
|
17 |
raise
|
Wenn Sie diesem Muster folgen (vorausgesetzt, Sie haben die Protokollierung korrekt eingerichtet), haben Sie unabhängig davon, was passiert, in Ihren Protokollen eine ziemlich gute Aufzeichnung darüber, was schief gelaufen ist, und können das Problem beheben.
Stellen Sie beim erneuten Erhöhen sicher, dass Sie nicht immer wieder dieselbe Ausnahme auf verschiedenen Ebenen protokollieren. Dies ist eine Verschwendung, die Sie verwirren und Sie glauben lassen kann, dass mehrere Instanzen desselben Problems aufgetreten sind, als in der Praxis eine einzelne Instanz mehrmals protokolliert wurde.
Der einfachste Weg, dies zu tun, besteht darin, alle Ausnahmen weitergeben zu lassen (es sei denn, sie können sicher behandelt und früher verschluckt werden) und anschließend die Protokollierung nahe der obersten Ebene Ihrer application/system durchzuführen.
Sentry
Protokollierung ist eine Funktion. Die häufigste Implementierung ist die Verwendung von Protokolldateien. Für verteilte Großsysteme mit Hunderten, Tausenden oder mehr Servern ist dies jedoch nicht immer die beste Lösung.
Ein Dienst wie Sentry ist sehr hilfreich, um Ausnahmen in Ihrer gesamten Infrastruktur im Auge zu behalten. Es zentralisiert alle Ausnahmeberichte und fügt zusätzlich zum Stacktrace den Status jedes Stack-Frames hinzu (den Wert der Variablen zum Zeitpunkt der Auslösung der Ausnahme). Es bietet auch eine wirklich schöne Oberfläche mit Dashboards, Berichten und Möglichkeiten, die Nachrichten nach mehreren Projekten aufzuteilen. Es ist Open Source, sodass Sie Ihren eigenen Server ausführen oder die gehostete Version abonnieren können.
Umgang mit vorübergehenden Fehlern
Einige Fehler sind vorübergehend, insbesondere beim Umgang mit verteilten Systemen. Ein System, das beim ersten Anzeichen von Problemen ausflippt, ist nicht sehr nützlich.
Wenn Ihr Code auf ein Remote-System zugreift, das nicht reagiert, besteht die herkömmliche Lösung aus Zeitüberschreitungen, aber manchmal ist nicht jedes System mit Zeitüberschreitungen ausgestattet. Zeitüberschreitungen sind nicht immer einfach zu kalibrieren, wenn sich die Bedingungen ändern.
Ein anderer Ansatz besteht darin, schnell zu versagen und es dann erneut zu versuchen. Der Vorteil ist, dass Sie, wenn das Ziel schnell reagiert, nicht viel Zeit im Schlaf verbringen müssen und sofort reagieren können. Wenn dies jedoch fehlschlägt, können Sie es mehrmals wiederholen, bis Sie feststellen, dass es wirklich nicht erreichbar ist, und eine Ausnahme auslösen. Im nächsten Abschnitt stelle ich einen Dekorateur vor, der das für Sie erledigen kann.
Hilfreiche Dekorateure
Zwei Dekoratoren, die bei der Fehlerbehandlung helfen können, sind der @log_error, der eine Ausnahme protokolliert und dann erneut auslöst, und der @retry-Dekorator, der wiederholt versucht, eine Funktion mehrmals aufzurufen.
Fehlerprotokollierer
Hier ist eine einfache Implementierung. Der Dekorateur nimmt ein Logger-Objekt aus. Wenn eine Funktion dekoriert und die Funktion aufgerufen wird, wird der Aufruf in eine Try-Except-Klausel eingeschlossen. Wenn eine Ausnahme aufgetreten ist, wird sie protokolliert und die Ausnahme schließlich erneut ausgelöst.
1 |
def log_error(logger) |
2 |
|
3 |
def decorated(f): |
4 |
|
5 |
@functools.wraps(f) |
6 |
|
7 |
def wrapped(*args, **kwargs): |
8 |
|
9 |
try: |
10 |
|
11 |
return f(*args, **kwargs) |
12 |
|
13 |
except Exception as e: |
14 |
|
15 |
if logger: |
16 |
|
17 |
logger.exception(e) |
18 |
|
19 |
raise
|
20 |
|
21 |
return wrapped |
22 |
|
23 |
return decorated |
So verwenden Sie es:
1 |
import logging |
2 |
|
3 |
logger = logging.getLogger() |
4 |
|
5 |
|
6 |
|
7 |
@log_error(logger) |
8 |
|
9 |
def f(): |
10 |
|
11 |
raise Exception('I am exceptional') |
Retrier
Hier ist eine sehr gute Implementierung des @retry decorator.
1 |
import time |
2 |
|
3 |
import math |
4 |
|
5 |
|
6 |
|
7 |
# Retry decorator with exponential backoff
|
8 |
|
9 |
def retry(tries, delay=3, backoff=2): |
10 |
|
11 |
'''Retries a function or method until it returns True.
|
12 |
|
13 |
|
14 |
|
15 |
delay sets the initial delay in seconds, and backoff sets the factor by which
|
16 |
|
17 |
the delay should lengthen after each failure. backoff must be greater than 1,
|
18 |
|
19 |
or else it isn't really a backoff. tries must be at least 0, and delay
|
20 |
|
21 |
greater than 0.'''
|
22 |
|
23 |
|
24 |
|
25 |
if backoff <= 1: |
26 |
|
27 |
raise ValueError("backoff must be greater than 1") |
28 |
|
29 |
|
30 |
|
31 |
tries = math.floor(tries) |
32 |
|
33 |
if tries < 0: |
34 |
|
35 |
raise ValueError("tries must be 0 or greater") |
36 |
|
37 |
|
38 |
|
39 |
if delay <= 0: |
40 |
|
41 |
raise ValueError("delay must be greater than 0") |
42 |
|
43 |
|
44 |
|
45 |
def deco_retry(f): |
46 |
|
47 |
def f_retry(*args, **kwargs): |
48 |
|
49 |
mtries, mdelay = tries, delay # make mutable |
50 |
|
51 |
|
52 |
|
53 |
rv = f(*args, **kwargs) # first attempt |
54 |
|
55 |
while mtries > 0: |
56 |
|
57 |
if rv is True: # Done on success |
58 |
|
59 |
return True |
60 |
|
61 |
|
62 |
|
63 |
mtries -= 1 # consume an attempt |
64 |
|
65 |
time.sleep(mdelay) # wait... |
66 |
|
67 |
mdelay *= backoff # make future wait longer |
68 |
|
69 |
|
70 |
|
71 |
rv = f(*args, **kwargs) # Try again |
72 |
|
73 |
|
74 |
|
75 |
return False # Ran out of tries :-( |
76 |
|
77 |
|
78 |
|
79 |
return f_retry # true decorator -> decorated function |
80 |
|
81 |
return deco_retry # @retry(arg[, ...]) -> true decorator |
Abschluss
Die Fehlerbehandlung ist sowohl für Benutzer als auch für Entwickler von entscheidender Bedeutung. Python bietet hervorragende Unterstützung in der Sprach- und Standardbibliothek für die ausnahmebasierte Fehlerbehandlung. Wenn Sie die Best Practices sorgfältig befolgen, können Sie diesen oft vernachlässigten Aspekt überwinden.



