1. Code
  2. Python

Professionelle Fehlerbehandlung mit Python

Scroll to top

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.