Einfache Fehlerkontrolle#

Warum Fehlerkontrolle?#

  • Fakt ist, jeder macht Fehler

  • Code läuft beim ersten Mal nie richtig

  • Großteil der Entwicklungszeit wird mit Fehlersuche (Debugging) verbracht (Besonders bei Einsatz von KI -> Später mehr dazu)

  • “Profis” unterscheiden sich von “AnfängerInnen” dadurch, dass sie Fehler systematisch und effizient finden und beheben können

  • Syntaktische Fehler (Syntax Errors) sind leicht einzugrenzen (Fehlermeldung enthält Zeilennummer)

  • Laufzeitfehler (Runtime Errors) sind schwieriger zu finden

    • Programm läuft, tut aber nicht das, was es soll

    • Fehler fallen oft erst auf, wenn das Programm schon beim Kunden ist

Fehler vermeiden#

  • Programmier-Konventionen beachten

    • z.B. bei Namen für Variablen, Funktionen etc.

  • Kommentarzeilen dort, wo im Code etwas passiert

    • z.B. Verzweigung mit nicht offensichtlicher Bedingung

    • z.B. Funktionen (Zweck, Input, Output)

  • jede Funktion hat nur eine Funktionalität

    • jede Funktion einzeln & sofort testen

    • Wenn später Funktion verwendet wird, kann ein etwaiger Fehler dort nicht mehr sein!

      • d.h. kann Fehler im Programm schneller lokalisieren!

  • jede Funktionalität hat eigene Funktion

    • Programm in überschaubare Funktionen zerlegen!

  • nicht alles auf einmal programmieren!

    • Achtung: Häufiger Anfängerfehler!

  • Möglichst viele Fehler bewusst abfangen!

    • Funktions-Input auf Konsistenz prüfen!

      • Fehler-Abbruch, falls inkonsistent!

    • garantieren, dass Funktions-Output zulässig!

Try-Except#

  • try-Block: Code, der potentiell Fehler produziert

  • except-Block: Code, der ausgeführt wird, wenn im try-Block ein Fehler auftritt

  • Optional: else-Block: Code, der ausgeführt wird, wenn im try-Block kein Fehler auftritt

  • Optional: finally-Block: Code, der immer ausgeführt wird, unabhängig davon, ob ein Fehler aufgetreten ist oder nicht

try:
    # Code, der potentiell Fehler produziert
except:
    # Code, der ausgeführt wird, wenn im try-Block ein Fehler auftritt

Einfaches Beispiel:#

Das folgende Beispiel hat keinen praktischen Nutzen, sondern soll nur die Funktionsweise von try-except demonstrieren.

def multiply(x,y):
    try:
        print(f"The product is {x*y}.")
    except:
        print("Error: Input must be a number.")
    else:
        print("Thanks for using our multiplication service!")
    finally:
        print("Goodbye!")

multiply(3,4)  # Valid input
multiply("Hallo",5) # Input that does not cause an error, but probably not intended
multiply("Hallo","Welt") # Invalid input
The product is 12.
Thanks for using our multiplication service!
Goodbye!
The product is HalloHalloHalloHalloHallo.
Thanks for using our multiplication service!
Goodbye!
Error: Input must be a number.
Goodbye!

Bestimmte Fehlertypen abfangen#

  • Exception ist die Basisklasse für alle eingebauten Fehler, die Python abfangen kann

  • Erlaubt die Ausgabe von Fehlertyp

def divide(a, b):
    try:
        result = a / b
    except Exception as e:
        print(f"Error: {e}")

divide(10,0)
divide(10,"Hallo")
Error: division by zero
Error: unsupported operand type(s) for /: 'int' and 'str'

Fehlertypen:

  • es gibt in Python viele eingebaute Fehlertypen, siehe https://docs.python.org/3/library/exceptions.html#concrete-exceptions für eine vollständige Liste

  • die für uns wichtigsten sind:

    • ZeroDivisionError: Division durch Null

    • TypeError: Operation oder Funktion wird auf einen Wert eines unpassenden Typs angewendet

    • ValueError: Operation oder Funktion erhält Argument mit dem richtigen Typ, aber unpassendem Wert

    • IndexError: Index ist außerhalb des gültigen Bereichs

    • RuntimeError: Allgemeiner Laufzeitfehler

    • OverflowError: Ergebnis einer arithmetischen Operation ist zu groß, um dargestellt zu werden

def divide(a, b):
    try:
        result = a / b
    except ZeroDivisionError:
        print("Error: Division by zero is not allowed.")
    except TypeError:
        print("Error: Both inputs must be numbers.")
    else:
        print(f"The result is {result}.")

# Example calls
divide(10, 2)      # Valid input
divide(10, 0)      # Division by zero
divide(10, "a")    # Invalid type
The result is 5.0.
Error: Division by zero is not allowed.
Error: Both inputs must be numbers.

Fehler bewusst auslösen#

  • raise-Anweisung: Löst einen Fehler aus

  • Nützlich um dem User zu signalisieren, dass etwas schief gelaufen ist

  • Besser als print-Anweisung, da der User gezwungen wird, sich mit dem Fehler auseinanderzusetzen

  • Viel besser als das Programm einfach abstürzen zu lassen

def divide(a,b):
    if not (isinstance(a, (int, float)) and isinstance(b, (int, float))):
        raise TypeError("Both inputs must be numbers.")
    if b == 0:
        raise ValueError("Division by zero is not allowed.")
    return a / b

divide(10,0)  # Raises ValueError
divide(10,"Hallo")  # Raises TypeError
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
Cell In[4], line 8
      5         raise ValueError("Division by zero is not allowed.")
      6     return a / b
----> 8 divide(10,0)  # Raises ValueError
      9 divide(10,"Hallo")  # Raises TypeError

Cell In[4], line 5, in divide(a, b)
      3     raise TypeError("Both inputs must be numbers.")
      4 if b == 0:
----> 5     raise ValueError("Division by zero is not allowed.")
      6 return a / b

ValueError: Division by zero is not allowed.

assert-Anweisung#

  • assert-Anweisung überprüft, ob eine Bedingung wahr ist

  • Wenn die Bedingung falsch ist, wird eine AssertionError-Ausnahme ausgelöst

  • Nützlich für Debugging und Fehlerkontrolle

    • Fehler frühzeitig erkennen

assert condition, "Error message"

Beispiel: Euklidischer Algorithmus#

  • Stellt sicher, dass Input \(a,b\in\mathbb{N}\) erfüllt

def euklid(a,b):
    # Check that a and b are natural numbers (positive integers)
    assert isinstance(a, int) and isinstance(b, int) and a > 0 and b > 0, "a and b must be natural numbers (positive integers)"
    # compute gcd using Euclid's algorithm
    while b != 0:
        a, b = b, a % b
    return a

print(euklid(48, 18))  # Valid input
print(euklid(48, -18)) # Invalid input
6
---------------------------------------------------------------------------
AssertionError                            Traceback (most recent call last)
Cell In[10], line 10
      7     return a
      9 print(euklid(48, 18))  # Valid input
---> 10 print(euklid(48, -18)) # Invalid input

Cell In[10], line 3, in euklid(a, b)
      1 def euklid(a,b):
      2     # Check that a and b are natural numbers (positive integers)
----> 3     assert isinstance(a, int) and isinstance(b, int) and a > 0 and b > 0, "a and b must be natural numbers (positive integers)"
      4     # compute gcd using Euclid's algorithm
      5     while b != 0:

AssertionError: a and b must be natural numbers (positive integers)

Testen von Code#

  • Testen ist der Prozess, ein Programm mit der Absicht auszuführen, Fehler zu finden!

    • Glenford Myers: Art of Software Testing (1979)

  • Test ist der Vergleich des Verhaltens eines Programms (Ist) mit dem erwarteten Verhalten eines Systems (Soll)

  • Es ist praktisch nicht möglich, alle Programmfunktionen und
    alle möglichen Werte in den Eingabedaten in allen Kombinationen zu testen.

    • d.h. Tests sind idR. unvollständig!

  • Probleme beim unvollständigen Testen

    • Tests erlauben nur das Auffinden von Fehlern

    • Tests können Korrektheit nicht beweisen

    • Fehlerursache ist durch Soll-Ist-Vergleich nicht zwangsläufig klar

    • Testfälle können selbst fehlerhaft sein!

  • Vorteile beim unvollständigen Testen

    • Zeitaufwand vertretbar

    • Tests beziehen sich idR. auf “realistischen Input”

    • Tests sind idR. reproduzierbar

Arten von Tests#

  • strukturelle Tests (für jede Funktion)

    • Werden alle Anweisungen ausgeführt oder gibt es toten Code?

    • Treten Fehler auf, wenn if ... else mit wahr / falsch durchlaufen werden?

  • funktionale Tests (für jede Fkt. und Programm)

    • Tut jede Funktion mit zulässigen Parametern das Richtige? (d.h. Ergebnis korrekt?)

    • Tut das Programm (bzw. Teilabschnitte) das Richtige? (d.h. Ergebnis korrekt?)

    • Werden unzulässige Parameter erkannt?

    • Werden Grenzfälle / Sonderfälle korrekt erkannt und liefern das Richtige?

    • Was passiert bei Fehleingaben, d.h. bei Fehler des Benutzers?

Funktionale Tests?#

  • Ziel: Tut Funktion / Programm das Richtige?

  • funktionale Tests brauchen Testfälle

    • mit bekanntem Ergebnis / Output

  • Was sind generische Fälle / Parameter?

    • Bei welchen Fällen treten Verzweigungen auf?

    • Möglichst viele Verzweigungen abdecken!

  • Welche Fälle sind kritisch?

    • Zahlen werden im Rechner nicht exakt dargestellt (später mehr!)

    • Außerdem keine exakte Arithmetik bei double! * jede double-Rechnung hat Rechenfehler!

    • Wo können aufgrund solcher Rechenfehler andere Ergebnisse auftreten? * z.B. Ist ein Punkt auf dem Kreisrand?

  • früh mit dem Testen beginnen

    • nach Implementierung jeder Funktion!

    • nicht erst dann, wenn Prg komplett fertig!

  • nach Code-Korrektur alle(!) Tests wiederholen

    • deshalb Dokumentation der Tests!

  • Ab jetzt in der UE stets: Wie wurde getestet?

    • allerdings nur inhaltlich

    • d.h. ohne Fehleingaben des Nutzers

Verwendung des VSCode Python Debuggers#

  • Debugger: Werkzeug zum Testen und Debuggen von Code

  • Eignet sich besonders um Laufzeitfehler zu finden

  • Ermöglicht das Setzen von Haltepunkten

  • Schrittweises Ausführen des Codes

  • Überwachen und Verändern von Variablenwerten

Debugger ist komfortabler als Kommentare und print-Anweisungen zum Debuggen!

Debugger starten#

Um ein Python Programm im Debugger-Modus zu starten, klicken Sie auf den kleinen Pfeil neben dem Play-Button und wählen Sie “Debug Python File”.

Run Debug Button Debugger View

Haltepunkte (Breakpoints)#

Das Programm wird ganz normal durchlaufen, außer man setzt Haltepunkte (Breakpoints).

  • Haltenpunkte können durch Klicken links neben die Zeilennummer gesetzt werden.

Breakpoint Example

Wenn Sie nun das Programm im Debugger-Modus starten, wird die Ausführung am ersten Haltenpunkt, der in der Programmausführung erreicht wird, angehalten.

Debugger Paused

Es ist möglich, mehrere Haltepunkte zu setzen. Das Programm wird dann nacheinander an jedem Haltepunkt angehalten.

Zusätzlich können Sie conditional breakpoints setzen, d.h. Haltepunkte, die nur dann ausgelöst werden, wenn eine bestimmte Bedingung erfüllt ist.

  • Klicken Sie dazu mit der rechten Maustaste auf den Haltepunkt und wählen Sie “Edit Breakpoint…”, oder

  • Klicken Sie mit der rechten Maustaste auf die Zeilennummer und wählen Sie “Add Conditional Breakpoint…”.

Conditional Breakpoint

Als Bedingung können Sie jeden Python-Ausdruck verwenden, der zu True oder False ausgewertet wird. Der Haltepunkt wird nur ausgelöst, wenn die Bedingung True ist.

  • Zum Beispiel können Sie einen Haltepunkt setzen, der nur ausgelöst wird, wenn eine Variable a einen bestimmten Wert hat: a == 42.

Variablen überwachen#

Im Debugger können Sie die Werte von Variablen überwachen.

  • Die Werte der Variablen der aktuellen Zeile werden angezeigt.

  • Durch klicken auf _images/debugger.png können Sie das “Variables” Fenster öffnen, in dem Sie die Werte aller Variablen sehen können.

Variables View

In der “Debug Console”

Debug Console

können Sie während des Debuggens Python Befehle ausführen, z.B. um den Wert von Variablen zu überprüfen/ändern oder um Funktionen aufzurufen.

Programmausführung steuern#

Am oberen Rand des Debugger-Fensters finden Sie verschiedene Buttons, um die Programmausführung zu steuern:

Debugger Controls

Von links nach rechts:

  • Continue: Setzt die Programmausführung fort bis zum nächsten Haltepunkt.

  • Step Over: Führt die nächste Zeile aus. Wenn die nächste Zeile ein Funktionsaufruf ist, wird die Funktion vollständig ausgeführt und die Ausführung wird in der nächsten Zeile nach dem Funktionsaufruf fortgesetzt.

  • Step Into: Führt die nächste Zeile aus. Wenn die nächste Zeile ein Funktionsaufruf ist, wird die Ausführung in die erste Zeile der Funktion fortgesetzt.

  • Step Out: Führt den Rest der aktuellen Funktion aus und setzt die Ausführung in der Zeile nach dem Funktionsaufruf fort.

  • Restart: Startet das Debugging von vorne.

  • Stop: Beendet das Debugging.

Der VSCode Debugger bietet noch viele weitere Funktionen, die Sie in der offiziellen Dokumentation unter https://code.visualstudio.com/docs/python/debugging nachlesen können.