Vorlesungsskript 21

Fehler erzeugen, behandeln und vermeiden

Inzwischen haben Sie schon viel Pythoncode geschrieben und vermutlich festgestellt, dass nicht immer alles so funktioniert wie erwartet. Formale oder logische Fehler im Code können dazu führen, dass Ihr Pythonskript abgebrochen wird, statt ordnungsgemäß ausgeführt zu werden — oder dass es zwar ausgeführt wird, die Ergebnisse aber nicht korrekt sind. In diesem Vorlesungsskript erhalten Sie einen Überblick über häufig auftretende Fehler und lernen einige Methoden, um solche Fehler zu vermeiden bzw. zu behandeln, wenn sie doch auftreten.

Hilfe, mein Programm ist abgestürzt!

Keine Panik! Wenn Sie beim Aufruf eines Pythonskriptes eine Fehlermeldung erhalten, ist das nicht schlimm. Im Gegenteil: Sie können den Text der Fehlermeldung genau lesen und so die Stelle im Code identifizieren, die für den Absturz verantwortlich ist.

Die Sprache Python kennt eine Reihe verschiedener Fehlerarten. Die Fehlermeldungen, die im Problemfall angezeigt werden, enthalten immer mindestens zwei Informationen: Den Namen des Fehlers und die Zeilennummer, in der der Fehler aufgetreten ist. Erinnern Sie sich an eins der Codebeispiele aus Vorlesungsskript 20, in dem eine nicht existierende Gruppe ausgegeben werden sollte:

>>> drink = 'warm tea'
>>> description = re.compile('(hot|warm|cold)\s(milk|coffee|water|tea)')
>>> m = re.search(description, drink)
>>> m.group(0)
'warm tea'
>>> m.group(1)
'warm'
>>> m.group(2)
'tea'
>>> m.group(3)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
IndexError: no such group

Der hier auftretende Fehler heißt IndexError; das bedeutet, dass es nicht möglich ist, auf den angegebenen Index im angegebenen Objekt zuzugreifen. Der Fehler tritt in der Datei "<stdin>" auf; das bedeutet, dass der fehlerhafte Code direkt in das Terminal eingegeben wurde. (Wenn Sie den Code stattdessen in einer Datei namens aufgabe3.py speichern, wird an dieser Stelle also der Dateiname "aufgabe3.py" angezeigt.) Schließlich sehen wir, dass der Fehler sich in Zeile 1 befindet.

Beachten Sie, dass die fehlgeschlagene Zeile sich durch nichts von der Form oder Struktur der vorher ausgeführten Zeilen unterscheidet. Ob ein IndexError auftritt oder nicht, hängt davon ab, wie der Inhalt des vorliegenden Objekts (hier: m) beschaffen ist.

Traceback (most recent call last)

Der Traceback ist die Auflistung der aufgetretenen Fehler im Programm. Dabei bedeutet most recent call last, dass die Liste von oben nach unten chronologisch sortiert ist. Das spielt immer dann eine Rolle, wenn der fehlerhafte Code sich innerhalb einer Funktionsdefinition befindet. Betrachten wir diese Funktion, die das Ergebnis der Division zweier Zahlen zurückgeben soll:

def division(x,y):
    div = x/y
    return div

print(division(5,2))
print(division(10,10))
print(division(2,0))

Die Funktionsdefinition sieht auf den ersten Blick korrekt aus. Die ersten zwei Aufrufe der Funktion sind auch erfolgreich. Aber beim dritten Aufruf, mit den Argumenten 2 und 0, tritt ein Fehler auf: Durch null zu teilen ist nicht möglich. Die Ausgabe des Programms sieht so aus:

pythonista@pythonista-VirtualBox:~$ python3 div.py
2.5
1.0
Traceback (most recent call last):
  File "div.py", line 7, in <module>
    print(division(2,0))
  File "div.py", line 2, in division
    div = x/y
ZeroDivisionError: division by zero

In der ersten Zeile wird das Programm ausgeführt, das in der Datei div.py gespeichert ist. Die zwei folgenden Zeilen enthalten die Ergebnisse der ersten beiden Funktionsaufrufe, die problemlos berechnet werden können.

Als nächstes versucht der Python-Interpreter, die Funktion mit den Argumenten 2 und 0 auszuführen. Dieser Aufruf steht in Zeile 7 der Datei div.py (erster Teil des Tracebacks). Der Fehler tritt aber erst auf, wenn im Zuge des Funktionsaufrufs Zeile 2 der Datei div.py mit den Zahlen 2 und 0 ausgeführt wird (zweiter Teil des Tracebacks).

Der Traceback erlaubt es Ihnen, die Auswirkungen eines Fehlers in einem Teil des Programms auf den gesamten Ablauf des Programms zu verstehen. Es könnte zum Beispiel sein, dass Sie einfach eine falsche Funktion aufgerufen haben und der Fehler so entstanden ist. Dann müssen Sie nicht den Code der Funktion korrigieren, sondern die Stelle im Code, an der der falsche Funktionsaufruf steht. Der Traceback hilft Ihnen, indem er explizit auflistet, was der Interpreter in welcher Reihenfolge auszuführen versucht hat und an welcher Stelle er gescheitert ist.

Häufige Errors

Im Folgenden werden einige der häufigsten Arten von Fehlern aufgelistet und erläutert. In den bisherigen Übungsaufgaben haben Sie vermutlich schon einige dieser Errors beobachten können. Jetzt, wo Sie in der Lage sind, den Traceback zu lesen und zu verstehen, können Sie jederzeit in der Python-Dokumentation nachlesen, welche Bedeutung ein Error hat und wie Sie ihn behandeln können.

SyntaxError

>>> print len([1, 2, 3])
  File "<stdin>", line 1
    print len([1, 2, 3])
            ^
SyntaxError: invalid syntax

Jede Programmiersprache folgt fest definierten Regeln für die äußere Form des Codes, zum Beispiel bezüglich der notwendigen Einrückungen, Klammern, Doppelpunkten und so weiter. Fehler, die aus Verstößen gegen diese Regeln resultieren, sind für den Interpreter besonders einfach zu finden. Er meldet sogar Fehler in Funktionen, die im Programm niemals aufgerufen werden:

def missing_parentheses(arg1):
    print arg1

def invalid_syntax(arg1):
    print len(arg1)

invalid_syntax([3, 4, 5])

In diesem Programm werden zwei Funktionen definiert, die jeweils einen Formfehler enthalten (print() wird ohne Klammern verwendet). Beachten Sie, dass die erste definierte Funktion nie ausgeführt wird. Wenn das Programm ausgeführt wird, sehen wir folgenden Output:

pythonista@pythonista-VirtualBox:~$ python3 syntaxerror.py
  File "syntaxerror.py", line 2
    print arg1
             ^
SyntaxError: Missing parentheses in call to 'print'    

Die Fehlermeldung bezieht sich klar auf Zeile 2 des Programms, also auf den Funktionskörper einer Funktion, die später im Programm nicht verwendet wird. Der Pfeil in der vorletzten Zeile zeigt auf die Stelle, an der der Interpreter den Syntaxfehler entdeckt hat.

Beachten Sie, dass hier kein Traceback angezeigt wird, obwohl der Fehler sich in der Funktionsdefinition befindet. Das hängt damit zusammen, dass Syntaxfehler geprüft werden, bevor das Programm ausgeführt wird. Es gibt daher keine Informationen zur „Verschachtelung“ von Anweisungen. Außerdem wird nur einer der beiden Syntaxfehler im Code identifiziert. Erst, wenn Sie Zeile 2 korrigiert haben, setzt der Interpreter beim nächsten Programmstart die Prüfung der Syntax fort und stellt fest, dass auch Zeile 5 fehlerhaft ist.

IndentationError und TabError

Bei diesen beiden Fehlern handelt es sich um Untertypen von SyntaxError. Sie werden also auch vor dem Ausführen des Programms geprüft und vom Interpreter gemeldet.

In Python wird durch verschiedene Einrückungen der Codezeilen signalisiert, auf welcher logischen Ebene jede Zeile sich befindet. Alle Zeilen, die in einer gemeinsamen Einrückungsebene stehen, werden linear von oben nach unten nacheinander ausgeführt; um Zeilen einander unterzuordnen, wird Code unterhalb von Funktionskopfzeilen, if-Anweisungen oder for- bzw. while-Schleifenköpfen weiter nach rechts eingerückt. Sobald der Code auf die ursprüngliche Einrückungsebene zurückkehrt, endet der untergeordnete Block.

def indentationerror(number):
    print(number + 2)
     return number * 2 

Wenn sich die Einrückungsebene mitten im Code ändert, ohne dass sie durch eins der genannten Elemente eingeleitet wird, kann die Logik des Programms nicht interpretiert werden:

pythonista@pythonista-VirtualBox:~$ python3 indentationerror.py
  File "indentationerror.py", line 3
    return number * 2
    ^
IndentationError: unexpected indent

Übrigens fordert der Interpreter keine bestimmte Anzahl von Einrückungsleerzeichen. Er prüft nur, ob in aufeinander folgenden Zeilen die gleiche Anzahl von Leerzeichen am Anfang steht. Allerdings ist es guter Programmierstil, genau 4 Leerzeichen pro Einrückungsebene zu verwenden.

Im Gegensatz zum IndentationError ist der TabError fast unmöglich zu sehen:

def taberror(word1, word2):
        print(word1)
        print(word2)

Ein TabError entsteht dann, wenn eine Einrückungsebene sich nicht ausschließlich durch Tabs oder ausschließlich durch Leerzeichen vom restlichen Code absetzt, sondern durch eine Kombination von beidem. Der Grund dafür ist, dass ein Tab je nach Interpretation für eine beliebige Anzahl von Leerzeichen stehen kann, z.B. 4 oder 8. Durch diesen Interpretationsspielraum ist nicht editorübergreifend klar, welche Leerzeichenzahl genauso wie ein Tab behandelt werden soll. Sie müssen sich also zwischen Tabs und Leerzeichen entscheiden.

NameError

>>> fullname = "Esther Seyffarth"
>>> print(full_name)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'full_name' is not defined

Versuchen Sie, auf nichtdefinierte Variablen zuzugreifen, entsteht ein NameError. In diesem Beispiel wird die Variable full_name in der ersten Zeile mit Underscore angelegt, in der zweiten Zeile aber ohne Underscore referenziert.

Ein NameError tritt auch dann auf, wenn Sie versuchen, auf Variablen außerhalb ihres Gültigkeitsbereichs zuzugreifen. Beispielsweise bestehen Variablen, die Sie in einem Funktionskörper definieren, nur innerhalb dieser Funktion und können nicht außerhalb der Funktion referenziert werden.

AttributeError

>>> print(' Titel '.stip())
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'str' object has no attribute 'stip'

Falls Sie sich beim Aufrufen einer Funktion vertippen, wie hier bei strip(), tritt kein NameError auf, sondern ein AttributeError. Achtung: Dieser Fehler wird nicht nur durch Verschreiben ausgelöst. Was ist das Problem im folgenden Code?

>>> import re
>>> s = "Python"
>>> p = re.compile("th")
>>> m = re.search(p,s)
>>> print(p.group(0))
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: '_sre.SRE_Pattern' object has no attribute 'group'

Es macht Sinn, dass zwischen NameError und AttributeError unterschieden wird. Ersterer bezieht sich auf Variablen, die Sie definiert haben und über die Sie also die Kontrolle haben. Wenn Sie jedoch versuchen, auf ein Attribut zuzugreifen, das für einen Objekttyp nicht definiert ist, fehlt Ihnen diese Kontrolle. In dem Fall müssen Sie sich eine andere Strategie zum Korrigieren des Fehlers überlegen.

TypeError

Damit eine Operation in Python erfolgreich ist, müssen alle Operanden vom richtigen Typ sein. Ist das nicht der Fall, wird ein TypeError ausgelöst:

>>> 1 + "2"
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unsupported operand type(s) for +: 'int' and 'str'
>>> print("Ergebnis: " + 100)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: Can't convert 'int' object to str implicitly

Es gibt auch noch andere Situationen, in denen Sie auf einen TypeError stoßen können. Im folgenden Beispiel werden runde Klammern verwendet, wo der Interpreter eckige Klammern erwartet:

>>> d = {"the": 200, "of": 134}
>>> d("the")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'dict' object is not callable

Und schließlich kann es vorkommen, dass Sie in Ihrem eigenen Code Variablennamen wählen, mit denen Funktionen von Python überschrieben werden. Versuchen Sie unter allen Umständen, solche Situationen zu vermeiden! Sonst passiert folgendes:

>>> str(5)
'5'
>>> str = "Hello world"
>>> str
'Hello world'
>>> str(5)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'str' object is not callable

Die eingebaute Funktion str() wandelt das übergebene Argument in ein Objekt vom Typ String um. Im Codebeispiel wird eine Variable namens str angelegt, deren Wert ein String ist. Wenn nach dieser Zuweisung nun str im Code vorkommt, wird immer auf diesen String verwiesen, und Sie haben keinen Zugriff mehr auf die ursprüngliche Bedeutung von str als Funktion.

ValueError

Wie Sie gesehen haben, ist es wichtig, dass jedes im Code vorkommende Objekt jeweils vom richtigen Typ ist. Allerdings reicht es nicht aus, wenn der Typ stimmt: Für viele Operationen ist es wichtig, dass nicht nur der Typ korrekt ist, sondern auch bestimmte Bedingungen über den Wert des Objektes erfüllt sind:

>>> int("5")
5
>>> int("5.1")
Traceback (most recent call last):
  File "<stdin>", line 1, in in <module>
ValueError: invalid literal for int() with base 10: '5.1'
>>> int("twenty")
Traceback (most recent call last):
  File "<stdin>", line 1, in in <module>
ValueError: invalid literal for int() with base 10: 'twenty'

In der ersten Zeile wird der String "5" erfolgreich in eine Integerzahl umgewandelt. Bei den Strings "5.1" und "twenty" funktioniert das allerdings nicht. Die Umwandlungsfunktion int() verlangt ein Stringargument, und es werden in allen Zeilen Strings übergeben. Im zweiten und dritten Aufruf können die übergebenen Strings aber nicht korrekt verarbeitet werden, sodass der ValueError entsteht.

KeyError

Der KeyError ist verwandt mit dem oben schon erwähnten IndexError. Beide gehören in die Kategorie LookupError, treten also auf, wenn in einer Datenstruktur ein Element gesucht wird, das nicht vorhanden ist. Beim IndexError lag das daran, dass die angegebene Position im String oder der Liste nicht existiert, z.B. weil der Index größer ist als der größte vorhandene Index.

Löst Ihr Code einen KeyError aus, bedeutet das, dass in einem Dictionary der gesuchte Schlüssel nicht gefunden werden kann:

>>> noten = {"Mathe": 1.3, "Deutsch": 2.3, "Englisch": 1.7}
>>> noten["Latein"]
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
KeyError: 'Latein'

FileNotFoundError

Wenn Sie versuchen, eine Datei zu lesen, die nicht existiert, wird ein FileNotFoundError ausgelöst:

>>> open("thisfiledoesnotexist.txt", "r")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
FileNotFoundError: [Errno 2] No such file or directory: 'thisfiledoesnotexist.txt'

Achtung: Beim Öffnen von Dateien geben Sie den Modus an, in dem sie geöffnet werden sollen: "r" zum Lesen, "w" zum Schreiben. Der Error wird nur dann ausgelöst, wenn Sie als Modus "r" angeben. Andernfalls wird die Datei einfach vom Python-Interpreter angelegt.

>>> open("thisfiledoesnotexist.txt", "w")
<_io.TextIOWrapper name='thisfiledoesnotexist.txt' mode='w' encoding='UTF-8'>

try und except

Sie kennen nun einige Fehlerarten in Python und können am Traceback ablesen, wie ihr Code korrigiert werden muss. In den Fällen, wo ein Error durch einen Tipp- oder Denkfehler Ihrerseits zustande gekommen ist, können Sie einfach die fehlerhafte Zeile verbessern. Es kann aber auch vorkommen, dass Fehler erst während der Programmausführung entstehen, zum Beispiel weil bestimmte Eingaben vom Benutzer nicht verarbeitet werden können.

def polish_notation(calc_input):
    calc_input = calc_input.split()
    arg1 = calc_input[0]
    op = calc_input[1]
    arg2 = calc_input[2]
    result = op + " " + arg1 + " " + arg2
    return result

while True:
    original_calc = input("Bitte Term eingeben: ")
    polish_notation_calc = polish_notation(original_calc)
    print("Polnische Notation Ihrer Eingabe: " + polish_notation_calc)
    print("")

Der Code im Beispiel hat die Funktion, Rechenterme umzustellen, sodass der Operator nicht mehr zwischen den Operanden steht, sondern davor. Die Funktion erwartet Eingaben der Form [arg1] [operator] [arg2], beispielsweise 5 + 5. Sehen wir uns einmal an, wie das während der Laufzeit aussieht:

pythonista@pythonista-VirtualBox:~$ python3 polishnotation.py 
Bitte Term eingeben: 5 * 3 
Polnische Notation Ihrer Eingabe: * 5 3

Bitte Term eingeben: 10 + 8
Polnische Notation Ihrer Eingabe: + 10 8

Bitte Term eingeben: 3*4
Traceback (most recent call last):
  File "polishnotation.py", line 11, in <module>
    polish_notation_calc = polish_notation(original_calc)
  File "polishnotation.py", line 4, in polish_notation
    op = calc_input[1]
IndexError: list index out of range

Die Benutzerin hat sich nicht an das erwartete Format gehalten! Die Funktion geht davon aus, dass Operanden und Operator durch Leerzeichen voneinander getrennt sind. Ist das nicht der Fall, kann das Programm seine Aufgabe nicht erfüllen.

Wenn absehbar ist, welche Fehlerart in einem Teil des Programmcodes auftreten wird, können Sie eine Fehlerbehandlung in Ihr Programm einbauen. Dazu umgeben Sie den „verdächtigen“ Teil des Programms mit einer Struktur, die im Fehlerfall das Problem abfängt und eine Lösung implementiert, die sinnvoll ist. Das kann zum Beispiel so aussehen:

def polish_notation(calc_input):
    calc_input = calc_input.split()
    try:
        arg1 = calc_input[0]
        op = calc_input[1]
        arg2 = calc_input[2]
        result = op + " " + arg1 + " " + arg2
    except IndexError:
        result = "Kann nicht berechnet werden. Prüfen Sie Ihre Eingabe."
    return result

Der IndexError trat ursprünglich dort auf, wo auf bestimmte Indizes in calc_input zugegriffen wurde. Diese Zeilen sind nun in einen try-Block eingebettet. Befehle, die in einem solchen try-Block stehen, werden zunächst ganz normal nacheinander ausgeführt. Der except-Block gibt an, welche Befehle ausgeführt werden sollen, falls der Code im try-Block einen Fehler auslöst.

Der Effekt von try und except ist, dass das Programm im Fehlerfall nicht mehr abstürzt. Stattdessen wird, falls ein IndexError auftritt, der Code aus dem except-Block ausgeführt und das Programm läuft weiter.

pythonista@pythonista-VirtualBox:~$ python3 polishnotation_try_except.py 
Bitte Term eingeben: 5 + 4
Polnische Notation Ihrer Eingabe: + 5 4

Bitte Term eingeben: 5-4
Polnische Notation Ihrer Eingabe: Kann nicht berechnet werden. Prüfen Sie Ihre Eingabe.

Bitte Term eingeben: 5 * 5
Polnische Notation Ihrer Eingabe: * 5 5

Die Zeile, die den except-Block einleitet, enthält die Information, welche Fehlerart abgefangen werden soll. Das bedeutet, dass Ihr Code auch mehrere except-Blöcke nacheinander enthalten kann. Sie können diese Angabe auch weglassen — dann wird Ihr Programm sämtliche Fehler, die es gibt, abfangen. Das wird allerdings als schlechter Stil angesehen, weil Sie so verstecken, welche Fehlerarten tatsächlich auftreten können. Außerdem ist für verschiedene Fehlerarten eine unterschiedliche Behandlung sinnvoll, und wenn Sie nicht zwischen Fehlerarten unterscheiden, ist Ihre Fehlerbehandlung zu allgemein, um wirklich nützlich zu sein.

try, except, else, finally

Die Programmstruktur mit try und except kann noch durch zwei weitere Blöcke ergänzt werden, die mit else: bzw. finally: eingeleitet werden. So kann der Code aussehen, wenn alle vier Blöcke vorhanden sind:

def polish_notation(calc_input):
    calc_input = calc_input.split()
    try:
        arg1 = calc_input[0]
        op = calc_input[1]
        arg2 = calc_input[2]
    except IndexError:
        result = "Kann nicht berechnet werden. Prüfen Sie Ihre Eingabe."
    else:
        result = op + " " + arg1 + " " + arg2
    finally:
        return result

Die ersten beiden Blöcke kennen Sie bereits. Der else-Block wird genau dann ausgeführt, wenn der Fehler nicht aufgetreten ist. Der finally-Block wird in jedem Fall ausgeführt, unabhängig davon, ob der try-Block erfolgreich war oder nicht.

else und finally zu verwenden, hilft Ihnen dabei, den try-Block so kurz wie möglich zu halten. Je mehr Zeilen in try eingebettet sind, umso mehr mögliche Fehlerursachen gibt es, sodass die Fehlerbehandlung weniger genau wird.

Validierung von Benutzereingaben: Look before You Leap vs. It Is Easier to Ask for Forgiveness than Permission

Ungültige Eingaben des Benutzers können Programme abstürzen lassen. Um das zu verhindern, müssen Eingaben validiert werden. Hierzu gibt es zwei Strategien: Entweder prüft das Programm erst ganz genau, ob alle Eingaben die richtige Form haben. Wenn nicht, gibt es eine Fehlermeldung aus. Wenn ja, verarbeitet es die Eingaben, wobei garantiert keine Exceptions auftreten. Diese Strategie wird mit dem Slogan Look before You Leap bezeichnet. Oder das Programm fängt einfach an, die Eingaben zu verarbeiten. Erst wenn dabei Exceptions auftreten, bemerkt das Programm, dass die Eingabe falsch war, fängt die Exception ab und reagiert mit einer entsprechenden Fehlermeldung. Für diese Strategie gibt es den Slogan It’s Easier to Ask for Forgiveness than Permission.

Hier ist noch einmal die Funktion polish_notation. Diesmal prüft sie sowohl, ob die Eingabe drei Elemente enthält, als auch, ob das erste und dritte Zahlen sind, und zwar nach der Strategie It’s Easier to Ask for Forgiveness than Permission:

def polish_notation(calc_input):
    calc_input = calc_input.split()
    try:
        arg1 = calc_input[0]
        op = calc_input[1]
        arg2 = calc_input[2]
    except IndexError:
        return "Bitte geben Sie drei durch Leerzeichen getrennte Elemente ein."
    try:
        arg1 = int(arg1)
        arg2 = int(arg2)
    except ValueError:
        return "Das erste und dritte Element müssen Zahlen sein."
    return op + " " + str(arg1) + " " + str(arg2)

Hier ist eine Version, die dasselbe macht, allerdings mit der Strategie Look before You Leap. Sie versucht nicht einfach, auf die Indizes 0-2 zuzugreifen und die Elemente an den Indizes 0 und 2 in Integers umzuwandeln, sondern überprüft erst, ob das überhaupt möglich ist:

def polish_notation(calc_input):
    calc_input = calc_input.split()
    if len(calc_input) != 3:
        return "Bitte geben Sie drei durch Leerzeichen getrennte Elemente ein."
    arg1 = calc_input[0]
    op = calc_input[1]
    arg2 = calc_input[2]
    int_pattern = re.compile('^\d+$')
    if not (int_pattern.match(calc_input[0]) and
            int_pattern.match(calc_input[2]):
        return "Das erste und dritte Element müssen Zahlen sein."
    arg1 = int(arg1)
    arg2 = int(arg2)
    return op + " " + str(arg1) + " " + str(arg2)

Welche Strategie man anwendet, ist teils eine Geschmacksfrage. Look before You Leap ist oft klarer, da man sofort sieht, bis wohin die Funktion im Falle eines Fehlers ausgeführt wird, wohingegen das bei mehrzeiligen try-Blöcken nicht auf den ersten Blick klar ist. Es gibt trotzdem einige Situationen, in denen es tatsächlich besser ist, hinterher ggf. um Verzeihung zu bitten (also eine Exception abzufangen) statt vorher um Erlaubnis:

  1. Es kann sein, dass Look before You Leap nicht gut möglich ist, weil man nicht weiß, wie man genau überprüfen soll, ob die Eingabe später zu einer Exception führen wird. Oben haben wir z.B. für Ganzzahlen mal eben schnell einen regulären Ausdruck definiert, aber wie sieht es aus, wenn wir Kommazahlen akzeptieren wollen? Wie sieht ein regulärer Ausdruck dafür aus? Können wir sicher sein, damit alle möglichen gültigen Eingaben abzudecken? Es ist einfacher, Pythons float-Funktion zu benutzen und ggf. eine Exception abzufangen.
  2. Es kann auch ineffizient sein, erst einen vorsichtigen Look vorzunehmen, wenn im Rahmen des späteren Leap ohnehin noch einmal eine Prüfung erfolgt: der Computer muss dann zweimal prüfen. Z.B. mussten wir hier erst einen regulären Ausdruck kompilieren und anwenden, was relativ rechenintensive Operationen sind. Effizienter ist es, den Leap einfach zu versuchen, mit einem try/except als Sicherheitsnetz.
  3. Es kann auch sein, dass ein verlässlicher Look unmöglich ist, weil sich bis zum Leap die Welt möglicherweise geändert hat. Hier ist zum Beispiel ein Codeschnipsel, der vor dem Öffnen einer Datei erst prüft, ob sie existiert:
    import os
    
    def get_file_contents(path):
        """Returns the contents of a file.
    
        If the file does not exist, the empty string is returned."""
        if not os.path.isfile(path):
            return ''
        infile = open(path, 'r')
        result = infile.read()
        infile.close()
        return result
    Dieser Check ist aber nicht verlässlich: Wenn durch einen dummen Zufall ein anderes Programm just im falschen Moment die Datei löscht, ist beim Aufruf von isfile die Datei noch vorhanden, beim wenig späteren Aufruf von open aber nicht mehr, und das Programm stürzt trotzdem ab. Besser ist es daher, von vornherein auf Exceptions gefasst zu sein:
    import os
    
    def get_file_contents(path):
        """Returns the contents of a file.
    
        If the file does not exist, the empty string is returned."""
        try:
            infile = open(path, 'r')
        except FileNotFoundError:
            return ''
        result = infile.read()
        infile.close()
        return result

Das bedeutet nicht, dass Sie jede Zeile Ihrer Programme ab sofort in die try/except-Struktur einbetten müssen. Stellen Sie sich die Frage, wie fehleranfällig jeder Sinnabschnitt Ihres Programms ist und unter welchen Bedingungen der Programmablauf beeinträchtigt werden könnte. In den praktischen Aufgaben für diese Woche werden Sie üben, Fehler während der Laufzeit mit try zu behandeln.

Zusätzliche Codebeispiele aus der Vorlesung