Ausnahmen (Exceptions)

Was sind Ausnahmen?

Wir haben schon mehrfach festgestellt, dass beim Ausführen von Programmen Fehler aufgetreten sind, die zum Abbruch des Programms geführt haben. Das passiert beispielsweise, wenn wir auf ein nicht existierendes Element einer Liste zuzugreifen versuchen:


In [ ]:
names = ['Otto', 'Hugo', 'Maria']
names[3]

Oder wenn wir versuchen eine Zahl durch 0 zu dividieren:


In [ ]:
user_input = 0 
2335 / user_input

Oder wenn wir versuchen, eine nicht vorhandene Datei zu öffnen:


In [ ]:
with open('hudriwudri.txt') as fh:
    print(fh.read())

Ausnahme-Typen

Wenn Python auf ein Problem stößt, erzeugt es ein Ausnahme-Objekt und beendet das Programm. Bei Bedarf haben wir allerdings die Möglichkeit, auf eine solche Ausnahme anders als mit einem Programmabbruch zu reagieren.

Dazu müssen wir den Code, der zu einer Ausnahme führen kann, in ein try ... except Konstrukt einbetten. Der fehleranfällige Codeteil steht im try-Block, gefolgt von einem except-Block, in dem steht, wie das Programm auf einen allfälligen Fehler reagieren soll.


In [ ]:
divisor = int(input('Divisor: '))
try:
    print(6543 / divisor)
except:
    print('Bei der Division ist ein Fehler aufgetreten')

Was wir hier gemacht haben, ist jedoch ganz schlechter Stil: Wir haben jede Art von Fehler oder Warnung abgefangen. Damit werden unter Umständen auch Fehler abgefangen, die wir gar nicht abfangen wollten, und die unter Umständen entscheidende Hinweise zur Fehlersuche geben könnten. Hier ein sehr konstruiertes Beispiel um das zu verdeutlichen:


In [ ]:
divisor = int(input('Divisor: '))
some_names = ['Otto', 'Anna']
try:
    print(some_names[divisor])
    print(6543 / divisor)
except:
    print('Bei der Division ist ein Fehler aufgetreten')

Hier noch einmal die drei Fragmente, mit denen wir am Anfang dieses Notebooks Fehler ausgelöst haben:


In [ ]:
names = ['Otto', 'Hugo', 'Maria']
names[3]

In [ ]:
user_input = 0 
2335 / user_input

In [ ]:
with open('hudriwudri.txt') as fh:
    print(fh.read())

Wenn wir genau hinsehen, stellen wir fest, dass es sich um drei unterschiedliche Arten von Fehlern handelt:

  • IndexError (names[3])
  • ZeroDivisionError (2335 / user_input)
  • FileNotFoundError (open('hudriwudri.txt'))

Python generiert also, abhängig davon, welche Art von Fehler aufgetreten ist, ein entsprechendes Ausnahme-Objekt. Alle diese Fehler sind abgeleitet vom allgemeinsten Ausnahme-Objekt (Exception) und damit Spezialfälle dieser Ausnahme. Diese speziellen Ausnahme-Objekte haben den Vorteil, dass wir abhängig vom Fehlertyp unterschiedlich darauf reagieren können. Wenn wir nur die Division durch 0 abfangen wollen, sieht der Code so aus:


In [ ]:
divisor = int(input('Divisor: '))
try:
    print(6543 / divisor)
except ZeroDivisionError:
    print('Division durch 0 ist nicht erlaubt.')

Wenn wir hingegen eine Nicht-Zahl (z.B. 'abc') eingeben, wird das Programm nach wie vor abgebrochen, weil hier ein ValueError ausgelöst wird, den wir nicht explizit abfangen.


In [ ]:
print(6543 / divisor)
try:
    divisor = int(input('Divisor: '))
except (ZeroDivisionError, ValueError):
    print('Bei der Division ist ein Fehler aufgetreten.')

Überlegen Sie, warum das Programm immer noch abgebrochen wird und versuchen Sie, das Problem zu lösen!

Wir können alternativ auch unterschiedliche except-Blöcke verwenden um auf unterschiedliche Fehler unterschiedlich zu reagieren:


In [ ]:
try:
    divisor = int(input('Divisor: '))
    print(6543 / divisor)
except ZeroDivisionError:
    print('Division durch 0 ist nicht erlaubt.')
except ValueError:
    print('Sie müssen eine Zahl eingeben!')

Falls wir ein Code-Fragment in jedem Fall ausführen wollen, also unabhängig davon, ob ein Fehler aufgetreten ist oder nicht, können wir einen finally-Block definieren. Da macht zum Beispiel Sinn, wenn wir irgendwelche Ressourcen wie Filehandles freigeben wollen.

try:
    f = open('data.txt')
    # make some computations on data
except ZeroDivisionError:
    print('Warning: Division by Zero in data.txt')
finally:
    f.close()

Exceptions bilden eine Hierarchie

Wir haben oben schon festgestellt, dass bestimmte Ausnahmen Spezialfälle von anderen Ausnahmen sind. Die allgemeinste Ausnahme ist Exception, von der alle anderen Ausnahmen abgeleitet sind. Ein ZeroDivisionError ist ein Spezialfall von ArithmethicError, was wiederum eine Spezialfall von Exception ist.

Diese Hierarchie von Ausnahmen können wir so sichtbar machen (das dient nur zur Illustration und muss nicht verstanden werden):


In [ ]:
import inspect
inspect.getmro(ZeroDivisionError)

Wir können uns diese Hierarchie zunutze machen, um gleichartige Fehler gemeinsam zu behandeln. Wollen wir alle ArithmeticError-Subtypen (OverflowError, ZeroDivisionError, FloatingPointError) gesammelt abfangen wollen, prüfen wir beim except auf diesen gemeinsamen Basistyp:


In [ ]:
try:
    divisor = int(input('Divisor: '))
    print(6543 / divisor)
except ArithmeticError:
    print('Kann mit der eingegebenen Zahl nicht rechnen.')

Die komplette Hierarchie der eingebauten Exceptions findet sich hier: https://docs.python.org/3/library/exceptions.html#exception-hierarchy

Exceptions wandern durch den Stack

Der große Vorteil von Exceptions ist, dass wir sie nicht zwingend dort abfangen müssen, wo sie auftreten, weil sie durch die Programmhierarchie durchgereicht werden, bis sie irgendwo behandelt werden (oder auch nicht, was dann zum Programmabbruch führt).


In [ ]:
def ask_for_int(msg):
    divisor = input('{}: '.format(msg))
    return int(divisor)

try:
    print(6543 / ask_for_int('Divisor eingeben'))
except ValueError:
    print('Ich kann nur durch eine Zahl dividieren!')

Im obigen Code tritt (wenn wir z.B. 'abc' eingeben) der ValueError in der Funktion ask_for_int() auf, wird aber im Hauptprogramm abgefangen. Das kann den Code unnötig komplex machen, weil wir beim Lesen des Codes immer feststellen müssen, wo der Fehler überhaupt herkommt, ermöglicht aber andererseits ein zentrales Ausnahme-Management.

Ausnahme-Objekte

Wenn eine Ausnahme auftritt, erzeugt Python ein Ausnahmeobjekt, das, wie wir gesehen haben, durchgereicht wird. Falls benötigt, können wir dieses Objekt sogar genauer untersuchen.


In [ ]:
def ask_for_int(msg):
    divisor = input('{}: '.format(msg))
    return int(divisor)

try:
    print(6543 / ask_for_int('Divisor eingeben'))
except ValueError as e:
    print('Ich kann nur durch eine Zahl dividieren!')
    print('Das Problem war: {}'.format(e.args))

Ausnahmen auslösen

Bei Bedarf können wir sogar selbst Ausnahmen auslösen.


In [ ]:
def ask_for_divisor():
    divisor = input('Divisor eingeben: ')
    if divisor == '0':
        raise ValueError('Divisor must not be 0!')
    return int(divisor)

try:
    print(6543 / ask_for_divisor())
except ValueError:
    print('Ungültige Eingabe')

Eigene Ausnahmen definieren

Manchmal ist es sehr nützlich, eigene Ausnahmen oder ganze Ausnahmehierarchien zu definieren um gezielt auf solche Ausnahmen reagieren zu können.


In [ ]:
class MyAppException(Exception): pass
class MyAppWarning(MyAppException): pass
class MyAppError(MyAppException): pass
class GradeValueException(MyAppError): pass

Damit haben wir unsere eigene Ausnahmehierarchie definiert:

Exception
|---- MyAppException
      | ---- MyAppWarning
      | ---- MyAppError
             | ---- GradeValueException

Je nach Bedarf können wir hier nun z.B. auf alle Subtypen von MyAppException reagieren oder auf alle MyAppWarnings oder MyAppErrors oder ganz gezielt auf eine GradeValueException.