8. Fehler und Ausnahmen

Bis jetzt wurden Fehlermeldungen nur am Rande erwähnt, aber wenn Du die Beispiele ausprobiert hast, hast Du sicherlich schon einige gesehen. Es gibt (mindestens) zwei verschiedene Arten von Fehlern: Syntaxfehler (engl.: syntax errors) und Ausnahmen (engl.: exceptions).

8.1. Syntaxfehler

Syntaxfehler, auch Parser-Fehler genannt, sind vielleicht die häufigsten Fehlermeldungen, die Du bekommt, wenn Du Python lernst:

>>> while True print('Hallo Welt')
  File "<stdin>", line 1, in ?
    while True print('Hallo Welt')
                   ^
SyntaxError: invalid syntax

Der Parser wiederholt die störende Zeile und zeigt mit einem kleinen ‘Pfeil’ auf die Stelle, an der der Fehler entdeckt wurde. Der Fehler ist an dem Token aufgetreten (oder wurde zumindest dort entdeckt), welches vor dem Pfeil steht: In dem Beispiel wurde der Fehler bei der print()-Funktion entdeckt, da ein Doppelpunkt (':') vor der Funktion fehlt. Des weiteren werden der Dateiname und die Zeilennummer ausgegeben, sodass Du weißt, wo Du suchen musst, falls die Eingabe aus einem Skript kam.

8.2. Ausnahmen

Selbst wenn eine Anweisung oder ein Ausdruck syntaktisch korrekt ist, kann es bei der Ausführung zu Fehlern kommen. Fehler, die bei der Ausführung auftreten, werden Ausnahmen (engl: exceptions) genannt und sind nicht notwendigerweise schwerwiegend: Du wirst gleich lernen, wie Du in Python-Programmen mit ihnen umgehst. Die meisten Ausnahmen werden von Programmen aber nicht behandelt, und erzeugen Fehlermeldungen, wie dieses Beispiel zeigt:

>>> 10 * (1/0)
Traceback (most recent call last):
  File "<stdin>", line 1, in ?
ZeroDivisionError: int division or modulo by zero
>>> 4 + spam*3
Traceback (most recent call last):
  File "<stdin>", line 1, in ?
NameError: name 'spam' is not defined
>>> '2' + 2
Traceback (most recent call last):
  File "<stdin>", line 1, in ?
TypeError: Can't convert 'int' object to str implicitly

Die letzte Zeile der Fehlermeldung gibt an, was passiert ist. Es gibt verschiedene Typen von Ausnahmen, und der Typ der Ausnahme wird als Teil der Meldung ausgegeben: Die Typen in diesem Beispiel sind ZeroDivisionError, NameError und TypeError. Die Zeichenkette, die als Ausnahmetyp ausgegeben wird, ist der Name der eingebauten Ausnahme, die aufgetreten ist. Dies gilt für alle eingebauten Ausnahmen, muss aber für benutzerdefinierte Ausnahmen nicht zutreffen (es ist aber eine nützliche Konvention). Standard-Ausnahmen sind eingebaute Bezeichner (keine reservierten Schlüsselwörter).

Der Rest der Zeile gibt Details an, die auf dem Ausnahmetyp und darauf, was die Ausnahme ausgelöst hat, basieren.

Der vorangehende Teil der Fehlermeldung zeigt den Zusammenhang, in dem die Ausnahme auftrat, in Form eines Traceback. Im Allgemeinen wird dort der Programmverlauf mittels der entsprechenden Zeilen des Quellcodes aufgelistet; es werden jedoch keine Zeilen ausgegeben, die von der Standardeingabe gelesen wurden.

Built-in Exceptions listet alle eingebauten Ausnahmen und ihre Bedeutung auf.

8.3. Ausnahmen behandeln

Es ist möglich, Programme zu schreiben, welche ausgewählte Ausnahmen behandeln. Schau Dir das folgende Beispiel an, welches den Benutzer solange um Eingabe bittet, bis eine gültige Ganzzahl eingegeben wird, es dem Benutzer aber ermöglicht, das Programm abzubrechen (mit Strg-C oder was das Betriebssystem sonst unterstützt); ein solcher vom Benutzer erzeugter Abbruch löst eine KeyboardInterrupt-Ausnahme aus:

>>> while True:
...     try:
...         x = int(input("Bitte gib eine Zahl ein: "))
...         break
...     except ValueError:
...         print("Ups! Das war keine gültige Zahl. Versuche es noch einmal...")
...

Die try-Anweisung funktioniert folgendermaßen:

  • Zuerst wird der try-Block (die Anweisung(en) zwischen den Schlüsselwörtern try und except) ausgeführt.
  • Wenn dabei keine Ausnahme auftritt, wird der except-Block übersprungen, und die Ausführung der try-Anweisung ist beendet.
  • Wenn während der Ausführung des try-Blocks eine Ausnahme auftritt, wird der Rest des Blockes übersprungen. Wenn dann der Typ dieser Ausnahme der Ausnahme gleicht, welche nach dem except-Schlüsselwort folgt, wird der except-Block ausgeführt, und danach ist die Ausführung der try-Anweisung beendet.
  • Wenn eine Ausnahme auftritt, welche nicht der Ausnahme im except-Block gleicht, wird sie an äußere try-Anweisungen weitergegeben; wenn keine passende try-Anweisung gefunden wird, ist die Ausnahme eine unbehandelte Ausnahme (engl: unhandled exception), und die Programmausführung stoppt mit einer Fehlermeldung wie oben gezeigt.

Eine try-Anweisung kann mehr als einen except-Block enthalten, um somit verschiedene Aktionen für verschiedene Ausnahmen festzulegen. Es wird höchstens ein except-Block ausgeführt. Ein Block kann nur die Ausnahmen behandeln, welche in dem zugehörigen try-Block aufgetreten sind, nicht jedoch solche, welche in einem anderen except-Block der gleichen try-Anweisung auftreten. Ein except-Block kann auch mehrere Ausnahmen gleichzeitig behandeln, dies wird in einem Tupel angegeben:

... except (RuntimeError, TypeError, NameError): ... pass

Der letzte except-Block kann ohne Ausnahme-Name(n) gelassen werden, dies fungiert als Wildcard. Benutze diese Möglichkeit nur sehr vorsichtig, denn dadurch können echte Programmierfehler verdeckt werden! Auf diese Weise kann man sich auch Fehlermeldungen ausgeben lassen und dann die Ausnahme erneut auslösen (sodass der Aufrufer diese Ausnahme ebenfalls behandeln kann):

import sys

try:
    f = open('myfile.txt')
    s = f.readline()
    i = int(s.strip())
except IOError as err:
    print("I/O error: {0}".format(err))
except ValueError:
    print("Konnte Daten nicht in Ganzzahl umwandeln.")
except:
    print("Unbekannter Fehler:", sys.exc_info()[0])
    raise

Die try ... except-Anweisung erlaubt einen optionalen else-Block, welcher, wenn vorhanden, nach den except-Blöcken stehen muss. Er ist nützlich für Code, welcher ausgeführt werden soll, falls der try-Block keine Ausnahme auslöst. Zum Beispiel:

for arg in sys.argv[1:]:
    try:
        f = open(arg, 'r')
    except IOError:
        print('Kann', arg, 'nicht öffnen')
    else:
        print(arg, 'hat', len(f.readlines()), 'Zeilen')
        f.close()

Die Benutzung eines else-Blockes ist besser, als zusätzlichen Code zum try-Block hinzuzufügen. Sie verhindert, dass aus Versehen Ausnahmen abgefangen werden, die nicht von dem Code ausgelöst wurden, welcher von der try ... except-Anweisung geschützt werden soll.

Wenn eine Ausnahme auftritt, kann sie einen zugehörigen Wert haben, das sogenannte Argument der Ausnahme. Ob ein solches Argument vorhanden ist und welchen Typ es hat, hängt vom Typ der Ausnahme ab.

Der except-Block kann einen Variablennamen nach dem Ausnahme-Namen spezifizieren. Der Variablenname wird an eine Ausnahmeinstanz gebunden und die Ausnahme-Argumente werden in instance.args gespeichert. Für die bessere Benutzbarkeit definiert eine Ausnahmeinstanz __str__(), sodass die Argumente direkt ausgegeben werden können, ohne dass .args referenziert werden muss. Man kann außerdem eine Ausnahme instantiieren bevor man sie auslöst, um weitere Attribute nach Bedarf hinzuzufügen:

>>> try:
...    raise Exception('spam', 'eggs')
... except Exception as inst:
...    print(type(inst))    # Die Ausnahmeinstanz
...    print(inst.args)     # Argumente gespeichert in .args
...    print(inst)          # __str__ erlaubt direkte Ausgabe von .args,
...                         # kann aber in Subklassen überschrieben werden
...    x, y = inst.args     # args auspacken
...    print('x =', x)
...    print('y =', y)
...
<class 'Exception'>
('spam', 'eggs')
('spam', 'eggs')
x = spam
y = eggs

Wenn eine Ausnahme Argumente hat, werden diese als letzter Teil (‘detail’) der Fehlermeldung unbehandelter Ausnahmen ausgegeben.

Ausnahme-Handler behandeln nicht nur Ausnahmen, welche direkt im try-Block auftreten, sondern auch solche Ausnahmen, die innerhalb von Funktionsaufrufen (auch indirekt) im try-Block ausgelöst werden. Zum Beispiel:

>>> def this_fails():
...     x = 1/0
...
>>> try:
...     this_fails()
... except ZeroDivisionError as err:
...     print('Behandle Laufzeitfehler:', err)
...
Behandle Laufzeitfehler: int division or modulo by zero

8.4. Ausnahmen auslösen

Die raise-Anweisung erlaubt es dem Programmierer, das Auslösen einer bestimmten Ausnahme zu erzwingen. Zum Beispiel:

>>> raise NameError('HeyDu')
Traceback (most recent call last):
  File "<stdin>", line 1, in ?
NameError: HeyDu

Das einzige Argument des Schlüsselwortes raise gibt die Ausnahme an, die ausgelöst werden soll. Es muss entweder eine Ausnahme-Instanz sein oder eine Ausnahmeklasse (eine Klasse, die von Exception erbt).

Wenn Du herausfinden willst, ob eine Ausnahme ausgelöst wurde, sie aber nicht behandeln willst, erlaubt Dir eine einfachere Form der raise-Anweisung, eine Ausnahme erneut auszulösen:

>>> try:
...     raise NameError('HeyDu')
... except NameError:
...     print('Eine Ausnahme flog vorbei!')
...     raise
...
Eine Ausnahme flog vorbei!
Traceback (most recent call last):
  File "<stdin>", line 2, in ?
NameError: HeyDu

8.5. Benutzerdefinierte Ausnahmen

Programme können ihre eigenen Ausnahmen benennen, indem sie eine neue Ausnahmeklasse erstellen (Unter Klassen gibt es mehr Informationen zu Python-Klassen). Ausnahmen sollten standardmäßig von der Klasse Exception erben, entweder direkt oder indirekt. Zum Beispiel:

>>> class MyError(Exception):
...     def __init__(self, value):
...         self.value = value
...     def __str__(self):
...         return repr(self.value)
...
>>> try:
...     raise MyError(2*2)
... except MyError as e:
...     print('Meine Ausnahme wurde ausgelöst, Wert:', e.value)
...
Meine Ausnahme wurde ausgelöst, Wert:: 4
>>> raise MyError('ups!')
Traceback (most recent call last):
  File "<stdin>", line 1, in ?
__main__.MyError: 'ups!'

In diesem Beispiel wurde die Methode __init__() der Klasse Exception überschrieben. Das neue Verhalten erzeugt schlicht das Attribut value, es ersetzt das Standardverhalten, ein Attribut args zu erzeugen.

Ausnahmeklassen können alle Möglichkeiten nutzen, die bei der Definition von Klassen zur Verfügung stehen, werden jedoch meist recht einfach gehalten; oft bieten sie nur eine Reihe von Attributen, welche genauere Informationen über den Fehler bereitstellen. Beim Erstellen von Modulen, welche verschiedene Fehler auslösen können, wird oft eine Basisklasse für Ausnahmen dieses Moduls definiert und alle anderen Ausnahmen für spezielle Fehlerfälle erben dann von dieser Basisklasse:

class Error(Exception):
    """Base class for exceptions in this module."""
    pass

class InputError(Error):
    """Exception raised for errors in the input.

    Attributes:
        expression -- input expression in which the error occurred
        message -- explanation of the error
    """

    def __init__(self, expression, message):
        self.expression = expression
        self.message = message

class TransitionError(Error):
    """Raised when an operation attempts a state transition that's not
    allowed.

    Attributes:
        previous -- state at beginning of transition
        next -- attempted new state
        message -- explanation of why the specific transition is not allowed
    """

    def __init__(self, previous, next, message):
        self.previous = previous
        self.next = next
        self.message = message

Meistens gibt man den Ausnahmen Namen, die auf “Error” enden, ähnlich der Namensgebung der Standardausnahmen.

Viele Standardmodule definieren ihre eigenen Ausnahmen, um Fehler zu melden, die in ihren Funktionen auftreten können. Mehr Informationen über Klassen sind in Kapitel Klassen zu finden .

8.6. Aufräumaktionen festlegen

Die try-Anweisung kennt einen weiteren optionalen Block, der für Aufräumaktionen gedacht ist, die in jedem Fall ausgeführt werden sollen. Zum Beispiel:

>>> try:
...     raise KeyboardInterrupt
... finally:
...     print('Auf Wiedersehen, Welt!')
...
Auf Wiedersehen, Welt!
KeyboardInterrupt
Traceback (most recent call last):
  File "<stdin>", line 2, in ?

Der finally-Block wird immer ausgeführt, bevor die try-Anweisung verlassen wird, egal ob eine Ausnahme aufgetreten ist oder nicht. Wenn eine Ausnahme im try-Block ausgelöst wurde, die nicht in einem except-Block behandelt wird (oder die in einem except-Block oder else-Block ausgelöst wurde), wird sie nach Ausführung des finally-Blocks erneut ausgelöst. Der finally-Block wird auch ausgeführt, wenn ein anderer Block der try-Anweisung durch eine break-, continue- or return-Anweisung verlassen wurde. Ein etwas komplizierteres Beispiel:

>>> def divide(x, y):
...     try:
...         result = x / y
...     except ZeroDivisionError:
...         print("Division durch Null!")
...     else:
...         print("Ergebnis ist:", result)
...     finally:
...         print("Führe finally-Block aus")
...
>>> divide(2, 1)
Ergebnis ist: 2.0
Führe finally-Block aus
>>> divide(2, 0)
Division durch Null!
Führe finally-Block aus
>>> divide("2", "1")
Führe finally-Block aus
Traceback (most recent call last):
  File "<stdin>", line 1, in ?
  File "<stdin>", line 3, in divide
TypeError: unsupported operand type(s) for /: 'str' and 'str'

Wie Du sehen kannst, wird der finally-Block in jedem Fall ausgeführt. Der TypeError, der durch die Division zweier Strings ausgelöst wird, wird nicht vom except-Block behandelt und wird somit erneut ausgelöst, nachdem der finally-Block ausgeführt wurde.

In echten Anwendungen ist der finally-Block nützlich, um externe Ressourcen freizugeben (wie Dateien oder Netzwerkverbindungen), unabhängig davon, ob die Ressource erfolgreich benutzt wurde oder nicht.

8.7. Vordefinierte Aufräumaktionen

Einige Objekte definieren Standard-Aufräumaktionen, die ausgeführte werden, wenn das Objekt nicht länger gebraucht wird, egal ob die Operation, die das Objekt benutzte, erfolgreich war oder nicht. Schau Dir das folgende Beispiel an, welches versucht, eine Datei zu öffnen und ihren Inhalt auf dem Bildschirm auszugeben.:

for line in open("myfile.txt"):
    print(line)

Das Problem dieses Codes ist, dass er die Datei, nachdem der Code ausgeführt wurde, für unbestimmte Zeit geöffnet lässt. In einfachen Skripten ist das kein Thema, aber in großen Anwendungen kann es zu einem Problem werden. Die with-Anweisung erlaubt es Objekten wie Dateien, auf eine Weise benutzt zu werden, dass sie stets korrekt und sofort aufgeräumt werden.

with open("myfile.txt") as f:
    for line in f:
        print(line)

Nachdem die Anweisung ausgeführt wurde, wird die Datei f stets geschlossen, selbst wenn ein Problem bei der Ausführung der Zeilen auftrat. Objekte die, wie Dateien, vordefinierte Aufräumaktionen bereitstellen, geben dies in ihrer Dokumentation an.