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: 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.
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
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
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 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"))
|
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) 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) 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) 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) 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) 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) |
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.
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.
|
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")) 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) |