11.3 BUGS & DEBUGGING

 

 

EINFÜHRUNG

 

Es gibt keine absolute Perfektion. Du kannst davon ausgehen, dass praktisch jede Software Fehler aufweist. Statt von Fehlern spricht man bei Software meistens von Bugs. Diese manifestieren sich beispielsweise dadurch, dass das Programm unter gewissen Umständen falsche Resultate produziert oder sogar abstürzt. Die Fehlersuche, Debugging genannt, ist daher fast ebenso wichtig wie das Programmieren selbst. Es ist allerdings selbstverständlich, dass jedermann, der Programmcode schreibt, sein Bestmögliches tun sollte, Bugs zu vermeiden. Im Wissen, dass Bugs allgegenwärtig sind, solltest du daher vorsichtig und defensiv programmieren. Bist du nicht ganz sicher, ob ein Algorithmus oder ein Codeteil richtig ist, so ist es besser, dass du dich mit ihm besonders intensiv beschäftigst, statt möglichst rasch darüber hinwegzugehen. Es gibt heutzutage viel Software, von deren richtigen Funktionieren grosse Geldsummen oder sogar Menschenleben auf dem Spiel stehen.  Als Programmierer von solcher missionskritischer Software (mission critical software), musst du bei jeder Zeile Code die Verantwortung übernehmen, dass sie korrekt arbeitet. Schnelle Hacks und das Prinzip von Trial and Error sind hier fehl am Platz.

Für die Entwicklung von Algorithmen und grossen Software-Systemen spielt die Zielsetzung, möglichst fehlerlose Programme zu erzeugen, eine wichtige Rolle. Es gibt viele Ansätze dazu: Man kann versuchen, die Korrektheit von Programmen ohne Einsatz des Computers mathematisch exakt zu beweisen, was allerdings nur mit kurzen Programmen gelingt. Eine andere Möglichkeit besteht darin, die  Syntax der Programmiersprache so einzuschränken, dass der Programmierer bestimmte Fehler gar nicht erst machen kann. Wichtigstes Beispiel ist die Elimination von Zeigervariablen (pointers), die bei unvorsichtiger Verwendung auf nicht definierte oder falsche Objekte verweisen. Es gibt darum Programmiersprachen mit vielen Einschränkungen dieser Art und solche, die dem Programmierer grosse Freiheiten lassen und dafür als weniger sicher gelten. Python gehört zur Klasse freiheitlicher Programmiersprachen und orientiert sich am Motto:
"Wir sind alle erwachsen und entscheiden selbst, was wir tun und lassen müssen" ("After all, we are all consenting adults here").

Ein wichtiges Prinzip zum Erstellen von möglichst fehlerfreier Software ist das Design by contract (DBC). Es geht auf den an der ETH Zürich wirkenden Bertrand Meyer zurück, dem Vater der Programmiersprache Eiffel. Er versteht Software als eine Vereinbarung zwischen dem Programmierer A und dem Anwender B, wobei B auch selbst Programmierer sein kann, der die von A entwickelten Module und Bibliotheken verwendet. In einem Vertrag legt A fest, unter welchen Bedingungen (preconditions) seine Module die richtigen Resultate liefern und beschreibt sie genau (postconditions). Anders gesagt: Hält sich Anwender B an die Preconditions, so hat er von A die Garantie, dass er ein Resultat gemäss den Postconditions erhält. Die Implementierung der Module braucht B nicht  zu kennen, A kann diese auch jederzeit ändern, ohne dass B davon betroffen ist.

 

 

ZUSICHERUNGEN (ASSERTIONS)

 

Da der Modulfabrikant A allerdings dem Anwender B etwas misstraut, baut er in seiner Software Tests ein, welche die geforderten Preconditions überprüfen. Diese werden Zusicherungen (assertions) genannt. Werden die Zusicherungen nicht eingehalten, so bricht das Programm gewöhnlich mit einer Fehlermeldung ab. (Es ist auch vorstellbar, dass der Fehler lediglich abgefangen wird, ohne dass das Programm abbricht.) Dabei kommt folgendes Prinzip der Softwareentwicklung zum Tragen:

Ein Programmabbruch mit einer möglichst klaren Beschreibung des Ursache (Fehlermeldung) ist besser als ein falsches Resultat.

In deinem Beispiel schreibst du als Programmierer A eine Funktion
sinc(x) (sinus cardinalis), die in der Signalverarbeitung eine wichtige Rolle spielt. Sie lautet:

Als Anwender B willst du die Funktionswerte im Bereich x = -20 .. 20 grafisch darstellen.

 

from gpanel import *
from math import pi, sin

def sinc(x):
    y = sin(x) / x
    return y

makeGPanel(-24, 24, -1.2, 1.2)
drawGrid(-20, 20, -1.0, 1, "darkgray")
title("Sinus Cardinalis: y = sin(x) / x")

x = -20
dx = 0.1
while x <= 20:
    y = sinc(x)
    if x == -20:
        move(x, y)
    else:
        draw(x, y)
    x += dx
Programmcode markieren (Ctrl+C kopieren, Ctrl+V einfügen)

 

Auf den ersten Blick scheint es keine Probleme zu geben, änderst du aber als Anwender das Inkrement von x auf 1, so gibt es einen bösen Crash.

Eine Lösung des Problems besteht darin, dass A als Precondition verlangt, dass x nicht 0 ist und  eine Assertion ausgibt, die den Fehler beschreibt.

from gpanel import *
from math import pi, sin

def sinc(x):
    assert x == 0, "Error in sinc(x). x = 0 not allowed"
    y = sin(x) / x
    return y

makeGPanel(-24, 24, -1.2, 1.2)
drawGrid(-20, 20, -1.0, 1, "darkgray")
title("Sinus Cardinalis: y = sin(x) / x")

x = -20
dx = 1
while x <= 20:
    y = sinc(x)
    if x == -20:
        move(x, y)
    else:
        draw(x, y)
    x += dx
Programmcode markieren (Ctrl+C kopieren, Ctrl+V einfügen)

 

Die Fehlermeldung ist jetzt bedeutend besser, da genau gesagt wird, wo er auftritt.

Viel besser wäre es allerdings, wenn A die Funktion so schreiben würde, dass für x = 0 der Grenzwert 1 der Funktion zurückgegeben wird.

 

from gpanel import *
from math import pi, sin

def sinc(x):
    if x == 0:
        return 1.0
    y = sin(x) / x
    return y

makeGPanel(-24, 24, -1.2, 1.2)
drawGrid(-20, 20, -1.0, 1, "darkgray")
title("Sinus Cardinalis: y = sin(x) / x")

x = -20
dx = 1
while x <= 20:
    y = sinc(x)
    if x == -20:
        move(x, y)
    else:
        draw(x, y)
    x += dx
Programmcode markieren (Ctrl+C kopieren, Ctrl+V einfügen)

Dieser Code widerspricht allerdings der Grundregel, dass Gleichheitstests mit Floats wegen möglicher Rundungsfehler gefährlich sind. Noch besser wäre es also, mit einer Epsilon-Schranke zu testen:

def sinc(x):
    epsilon = 1e-100
    if abs(x) < epsilon:
        return 1.0

Deine Funktion sinc(x) ist aber immer noch nicht sicher, dann eine weitere Precondition ist sicher, dass x eine Zahl sein muss. Ruft man sinc(x) mit dem Wert x = "python" auf, so ergibt sich wiederum ein böser Laufzeitfehler.

from math import sin

def sinc(x):
    y = sin(x) / x
    return y

print(sinc("python"))
Programmcode markieren (Ctrl+C kopieren, Ctrl+V einfügen)

Hier zeigt sich der Vorteil von Programmiersprachen mit einer Variablendeklarationen, denn dieser Fehler würde bereits vor der Programmausführung als Syntaxfehler entdeckt.

 

 

DEBUGGING-INFORMATION AUSSCHREIBEN

 

Da davon auszugehen ist, dass jedermann beim Schreiben von Programmen Fehler macht, ist es wichtig, Fehler möglichst rasch zu finden und zu beheben. Erfolgreiche Programmierer zeichnen sich dadurch aus, Fehler rasch zu eliminieren [mehr... Es gibt eine ganze Softwareentwicklung auf der Basis des Test Driven Development (TDD)].

def exchange(x, y):
    y = x
    x = y
    return x, y

a = 2
b = 3
a, b = exchange(a, b)
print(a, b)
Programmcode markieren (Ctrl+C kopieren, Ctrl+V einfügen)

Eine bekannte und einfache Strategie, um Fehler aufzufinden, besteht darin, an geeigneter Stelle die Werte von Variablen in die Konsole auszuschreiben:

def exchange(x, y):
    print("exchange() with params", x, y)
    y = x
    x = y
    print("exchange() returning", x, y)
    return x, y

a = 2
b = 3
a, b = exchange(a, b)
print(a, b)
Programmcode markieren (Ctrl+C kopieren, Ctrl+V einfügen)

Damit wird offensichtlich, wo der Fehler passiert und man kann ihn leicht beheben.

def exchange(x, y):
    print("exchange() with params", x, y)
    temp = y
    y = x
    x = temp
    print("exchange() returning", x, y)
    return x, y

a = 2
b = 3
a, b = exchange(a, b)
print(a, b)
Programmcode markieren (Ctrl+C kopieren, Ctrl+V einfügen)

Jetzt wo der Fehler behoben ist, sind die zusätzlichen Debug-Zeilen überflüssig. Statt sie zu löschen, kannst du sie auch nur auskommentieren, da du sie vielleicht später noch einmal benötigst.

def exchange(x, y):
#    print("exchange() with params", x, y)
    temp = y
    y = x
    x = temp
#    print("exchange() returning", x, y)
    return x, y

a = 2
b = 3
a, b = exchange(a, b)
print(a, b)
Programmcode markieren (Ctrl+C kopieren, Ctrl+V einfügen)

Elegant ist die Verwendung eines Debug-Flags, mit dem du Debug-Informationen an verschiedenen Stellen aktivieren oder deaktivieren kannst.

def exchange(x, y):
    if debug: print("exchange() with params", x, y)
    temp = y
    y = x
    x = temp
    if debug: print("exchange() returning", x, y)
    return x, y

debug = False
a = 2
b = 3
a, b = exchange(a, b)
print(a, b)
Programmcode markieren (Ctrl+C kopieren, Ctrl+V einfügen)

Wie du wahrscheinlich bereits weisst, kannst du in Python elegant Variablenwerte   ohne Verwendung einer Hilfsvariablen vertauschen. Du verwendest dabei das automatische Verpacken/Entpacken von Tuples

def exchange(x, y):
    x, y = y, x
    return x, y

a = 2
b = 3
a, b = exchange(a, b)
print(a, b)
Programmcode markieren (Ctrl+C kopieren, Ctrl+V einfügen)


 

VERWENDUNG DES DEBUGGERS

 

Debugger sind wichtige Hilfsmittel bei der Programmentwicklung von grossen Programmsystemen. Mit ihnen kannst du ein Programm langsam und sogar in Einzelschritten ausführen. Dabei wird sozusagen die enorme Ausführungsgeschwindigkeit des Computers dem beschränkten menschlichen Auffassungsvermögen angepasst. In TigerJython ist ein einfacher Debugger eingebaut, der dir auch helfen kann, den Programmablauf in einem korrekten Programm besser zu verstehen. Du untersuchst nun das oben verwendete, fehlerhafte Programm mit dem Debugger.

  Nachdem du es in den Editor genommen hast, klickst du auf den Debugger-Knopf


 
Mit dem Einzelschritt-Button kannst du das Programm Schritt um Schritt ausführen und dabei im Debugger-Fenster die Variablen beobachten. Nach 8x Klicken siehst du, dass vor der Rückkehr aus der Funktion exchange() die beiden Variablen x und y den Wert 2 haben.

Führst du nun den Bugfix aus, so kannst du beobachten, wie in exchange() die Variablen einander zugewiesen werden, was schliesslich zum richtigen Resultat führt. Gut sichtbar ist auch die kurze Lebensdauer der lokalen Variablen x und y gegenüber den globalen Variablen a und b.

Im Programm kannst du auch Haltepunkte (Breakpoints) setzen, damit du nicht mühsam im Einzelnschrittmodus unkritische Programmteile durchlaufen musst. Dazu klickst du ganz links auf die Zeilennummer-Spalte. Es erscheint eine kleine Flaggenikone, welche den Haltepunkt markiert.


Beim Drücken des Run-Buttons läuft nun das Programm bis zu dieser Stelle und hält an. Du kannst nun den momentanen Zustand der Variablen inspizieren und mit der Run-Button das Programm weiter führen oder mit dem Einzelschritt-Button schrittweise untersuchen.

Du kannst auch mehrere Haltepunkte setzen. Um einen Haltepunkt zu löschen, klickst du einfach auf die Flaggenikone.

 

 

FEHLERABFANG MIT EXCEPTIONS

 

Klassisch ist das Verfahren, Fehler mit Exceptions abzufangen. Man setzt dazu den kritischen Programmcode in einen try-Block. Tritt der Fehler auf, so wird der Block verlassen und das Programm fährt im except-Block weiter, wo du angepasst auf den Fehler reagieren musst. Im schlimmsten Fall, stoppst du die Programmausführung mit dem Aufruf von sys.exit().

Du kannst  beispielsweise den Fehler abfangen, falls in sinc(x) der Parameter kein numerischer Datentyp ist.

from sys import exit
from math import sin

def sinc(x):
    try:
        if x == 0:
            return 1.0
        y = sin(x) / x
    except TypeError:
        print("Error in sinc(x). x =", x, "is not a number")
        exit()
    return y

print(sinc("python"))
Programmcode markieren (Ctrl+C kopieren, Ctrl+V einfügen)

Die Postcondition für sinc(x)  könnte auch heissen, dass der Rückgabewert None ist, falls der Parameter einen falschen Typ hat. Es ist dann Aufgabe des Anwenders, diesen Fehler angepasst zu behandeln.

from math import sin

def sinc(x):
    try:
        if x == 0:
            return 1.0
        y = sin(x) / x
    except TypeError:
        return None
    return y

y = sinc("python")
if y == None:
   print("Illegal call")
else:
   print(y)
Programmcode markieren (Ctrl+C kopieren, Ctrl+V einfügen)