Download - Lösungsorientierte Fehlerbehandlung
Lösungsorientierte Fehlerbehandlung
Thomas Aglassinger
http://www.roskakori.athttps://github.com/roskakori/talks
Agenda
1.Begriffserklärung: Fehler
2.Darstellung von Fehler in Python
3.Lösungsorientierter Ansatz
4.Fehlermeldungen
5.Ähnliche Fehler gruppieren
6.Programmvorlage
7.Empfehlungen
Herausforderungen für Entwickler
● Was ist die Erwartung?→ notwendig, um Fehler erkennen zu können
● Was soll Programm im Fehlerfalle machen?● Wer kann den Fehler beheben?● Wie kann Programm bei der Korrektur
unterstützen?
Wozu Beispiele in Python?
● Leicht verständlich und gut lesbar● Kompakter Code● Weitgehend englische Sätze● Prozedural und objektorientiert nutzbar
Hinweis: Beispiele i.d.R. sowohl in Python 2 als auch 3 lauffähig, tw. mit geringfügigen Anpassungen für Python 3
Fehler in Python
● Darstellung über Exceptions● Durchgängige Verwendung in Standard-Bibliothek● Vorteil: nicht unabsichtlich ignorierbar● Bewährt in vielen anderen Programmiersprachen
(Java, C#, ...)● alternative Ansätze (hier nicht näher betrachtet):
● spezielle oder zusätzliche Rückgabewerte (zB go)● globale Fehlervariablen (zB errno in C)● Spezielle Sprachkonstrukte (zB „on error goto“ in
Basic)
Fehler erkennen
● Erkennen mit if und einer Fehlerbedingung
● Aufzeigen mit raise und einer Fehlermeldung● Fehler führt zum Abbruch der Routine
height) # Actual processing would happen here. pass
Fehler abfangen und ausgeben
● Aufruf des möglicherweise fehlschlagenden Codes mit try
● Bestimmte Fehler abfangen mit except
● Ausgabe: height is -3 but must be greater than 0
try: processSomething(-3) except ValueError as error: print(error)
vgl. C#, Java: catch
statt except
Ressourcen immer freigeben (1)
● Mit finally: sowohl bei Erfolg als auch Fehler
'some.txt', 'rb') processData(inputFile) finally:
Ressourcen immer freigeben (2)
● Mit with-Statement:
as inputFile:
● Voraussetzung: verwendete Klasse implementiert Context Manager→ hat Wissen darüber, was wie auf zu räumen ist
vgl. C#: using
Ressourcen immer freigeben (3a)
● Mit eigenem Context Manager● Schritt 1: Definition von __enter__() und __exit__()
class SocketClient(): '''Provide a ``socket`` and automatically close it when done.''' def __init__(self, host, port): self.socket = socket.create_connection((host, port))
return self
self.socket.shutdown(socket.SHUT_RDWR) self.socket.close()
Ressourcen immer freigeben (3b)
● Mit eigenem Context Manager● Schritt 2: Aufruf wie zuvor über with-Statement
'www.python.org', 80) as
pythonOrg.clientSocket.sendall( 'GET /index.html HTTP/1.0\n' + 'Host: www.python.org\n' +'\n') reply = pythonOrg.clientSocket.recv(64)
print(reply)
Ressourcen immer freigeben (4b)
● Mit eigenem Context Manager● Schritt 2: Aufruf wie zuvor über with-Statement
with SocketClient('www.python.org', 80) as pythonOrg: pythonOrg.clientSocket.sendall( 'GET /index.html HTTP/1.0\n' + 'Host: www.python.org\n' +'\n') reply = pythonOrg.clientSocket.recv(64)
print(reply)
Ergebnis von__enter__().
Aufruf von__exit__().
Aufruf von__init__().
Ressourcen freigeben (5)
● Nicht verwenden: __del__()● Aufruf erfolgt durch Garbage Collector● Nicht vorhersagbar wann → Bindet Ressource
unnötig lange● Wenn Exception während __del__(): nur Warnung
in Log, Aufrufer bekommt nichts davon mit● Daher nicht vorhersagbares Verhalten
→ Anwender glaubt, alles hat funktioniert→ Entwickler kann Fehler schwer reproduzieren
● Anwendung: Python-interne Aufräumarbeiten
vgl. Java: dispose()
Fehler erkennen mit assert (1)
● Beispiel von zuvor
def processSomething(height): if height <= 0: raise ValueError( 'height must be greater than 0')
def processSomething(height): assert height > 0, 'height must be greater than 0'
● Als Assertion:
Fehler erkennen mit assert (2)
● Wenn Bedingung verletzt:wie raise AssertionError('...')
● Deaktivieren von assert mittels Aufruf über:$ python -O xxx.py
(Buchstabe „großes O“, nicht Ziffer „0“)
● Von assert aufgerufene Funktionen dürfen keine Seiteneffekte haben → sonst unterschiedliches Programmverhalten je nachdem ob -O gesetzt
● Frage: wann raise und wann assert?→ Antwort folgt
Zusammenfassung
● Fehler erkennen mit raise und assert● Fehler abfangen mit try und except● Aufräumarbeiten: finally, with und
Context Manager● Nicht verwenden: __del__()
Grundprinzipen
● Im Zentrum der Überlegungen steht die Beseitigung des Fehlers (Lösung) und nicht der Fehler selbst
● Klare Zuständigkeiten zwischen Entwickler und Anwender
● Hilfreiche Fehlermeldungen● Fehlerbedingungen und -meldung aus
Programmcode ableitbar
Zuständigkeiten
● Entwickler: Umsetzung des Programms zur● Verarbeitung der Daten und Eingaben des
Anwenders● Liefern des gewünschten Ergebnisses
● Anwender:● Bereitstellen von Eingaben und Daten zur
Verarbeitung durch das Programm● Bereitstellen einer Umgebung, in der das
Programm ausführbar ist (ggf. über Administrator)
Nutzung von assert
● Fehlererkennung: aus internen Programmzustand● Lösung: Änderung des Programms● Zielgruppe für Fehlermeldungen: Entwickler● Klare Zuständigkeit beim Aufruf von Routinen:
muss Aufrufer oder Routine auf Fehlerbedingungen reagieren?
● Besonders nützlich zur Prüfung von übergebenen Parametern („preconditon“)
● Dient als „ausführbare“ Dokumentation
Nutzung von raise
● Fehlererkennung: aus Daten und in Umgebung● Lösung:
● Daten: korrekte und vollständige Eingabe● Umgebung: Dateien, Netzwerk, Berechtigungen, …
● Fehler erst zur Laufzeit erkennbar● Zielgruppe für Fehlermeldungen: Anwender
Anforderungen
● In Literatur oft: unklare Richtlinien („hilfreich“, „verständlich“, ...)
● In Praxis oft: Beschreibung, was falsch ist (z.B.„ungültiges Datum“)
● Lösungsorientierter Zugang:● Beschreibung des Ist-Zustands und des Soll-Zustands● Beschreibung der Maßnahmen, die zur Korrektur zu
setzen sind● Beschreibung oder Darstellung des Zusammenhangs,
in dem der Fehler aufgetreten ist
Ableiten der Fehlermeldung aus Programmcode
● Allgemein:
if height <= 0: raise ValueError( 'height is %d but must be greater than 0' % height)
● Konkret:
if actual != expected: raise SomeError('<actual> must be <expected>')
Darstellung des Zusammenhangs
● Bei raise: Anführen von Name und Wert des Ist-Zustands (z.B. „height is -3“)
● Bei except: ursprüngliche Fehlermeldung beibehalten und ergänzen:● Beschreiben der Herkunft der Fehlerursache (zB
Name und Position in Eingabedatei, Feldname in Formular, markieren in Benutzeroberfläche, ...)
● Beschreiben der Aktion, die aufgrund des Fehlers nicht durchführbar ist
Beispiel: Fehler erkennende Routine
def processSomething(height): if height <= 0: raise ValueError('height must be greater than 0')
# Actual processing would happen here. pass
def processSomething(height): if height <= 0: raise ValueError('height must be greater than 0')
# Actual processing would happen here. pass
Beispiel: Fehler berichtender Code
def processAllThings(dataFile): try: # Process all heights read from `dataFile`. lineNumber = 1 for line in dataFile:
except ValueError as error: print('cannot process %s, line %d: %s' %
Beispiel für Ausgabe im Fehlerfall:
cannot process some.txt, line 17: height is -3 but must be greater than 0
Wo Fehlermeldung ausgeben?
● Bei GUI oder Web-Anwendung:● Bei Eingaben: Feld hervorheben und Meldung unter
dem betroffenem Formularfeld● In eigenem Fehlerdialog oder auf Fehlerseite● Zusätzlich in Log für spätere Nachvollziehbarkeit
● Bei Services: in Log● Bei Befehlszeilenwerkzeugen: in Konsole auf
stderr
Ausgabe in Log-Datei
● Mit Standard Modul logging: http://docs.python.org/2/library/logging.html
● Mehrere Stufen zur Bewertung der Meldung, u.a.:● Info – Informationen, welche Aktionen gesetzt werden● Error – Fehlermeldungen● Exception – Fehlermeldung und Stack Trace● Debug – zusätzliche interne Detailinformationen;
interessant für Entwickler und während Fehleranalysen
● Ausgabe auf Datei, Console, Netzwerk-Socket, ...
Logging auf stderr
● Auch für Befehlzeilenanwendungen nutzbar
import
def processData(dataPath): _log.info(u'read "%s"', dataPath) with open(dataPath, 'rb') as dataFile: # Here we would actually process the data. pass
if __name__ == '__main__':
Logging auf stderr
● Was passiert im angeführten Beispiel, wenn die Datei data.txt nicht auffindbar ist?
Logging auf stderr
$ python somelog.py
INFO:some:read "data.txt"Traceback (most recent call last): File "somelog.py", line 15, in <module> processData('data.txt') File "somelog.py", line 8, in processData with open(dataPath, 'rb') as dataFile:IOError: [Errno 2] No such file or directory: 'data.txt'
$ echo $?1
Logging auf stderr
● Kein eigener Code für Fehlerbehandlung→ kein Aufwand für Entwickler
● Auch im Fehlerfalle Schließen der Datei→ effiziente Nutzung der Ressourcen
● Anzeige der I/O-Fehlermeldung→ Anwender kann Fehlermeldung nachgehen
● Exit Code 1→ etwaiges aufrufendes Shell-Script kann Fehler erkennen
● Nachteil: Stack Trace für Anwender verwirrend und auch nicht notwending, um Fehler zu beheben
Exception Hierarchie
● Exceptions sind Klassenhttp://docs.python.org/2/library/exceptions#exception-hierarchy
● Gruppierung von „ähnlichen“ Fehlern über Vererbungs-Hierarchie
● Ein try kann mehrere excepts haben● Über Reihenfolge können verschiedene Fehler
unterschiedlich behandelt werden
Lösungsorientiere Nutzung
● Vom Entwickler lösbar: AssertionError● Vom Anwender lösbar: EnvironmentError
→ Dateien, Netzwerk, Berechtigungen● Situationsabhängig vom Entwickler oder
Anwender lösbar: restliche Exception wie LookupError, ArithmeticError, ValueError, …→ hier ist Präzisierung durch Entwickler erforderlich
Alle anderen vom Anwender behebaren Fehler
● Mit except abfangen und umwandeln in eigene Exception, die klar als vom Anwender behebbar definiert ist
● Beispiel: DataError● Programm kann diese gleich wie EnvironmentError behandeln
● Mit if … raise selbst erkannte Fehler können gleich zu DataError führen
Beispiel DataError
● Für fehlerhafte Daten aus Eingangsdatei:
class DataError(Exception): pass
raise DataError('height is %d but must be greater than 0' \ % height)
Umwandeln einer Exception in DataError
Fehler abfangen und Meldung übernehmen:
try: # Process all heights read from `dataFile`. for lineNumber, line in enumerate(dataFile, start=1): processSomething(long(line))
% ( dataFile.name, lineNumber, error))
Umwandeln einer Exception in DataError
● In Python 3: Stack Trace erhalten mit Exception Chaining:
try: # Process all heights read from `dataFile`. for lineNumber, line in enumerate(dataFile, start=1): processSomething(long(line)) except ValueError as error: raise DataError('file %s, line %d' % (
● Ursprüngliche Exception und Fehlermeldung ist in __cause__ ersichtlich→ „gesamte“ Fehlermeldung zusammenbaubar
● Stack Trace enthält zuerst den ursprünglichen ValueError und anschließend den verketteten DataError
Alle anderen vom Entwickler behebaren Fehler
● Sind nun über „alles andere aber kein DataError“ erkennbar
● Behandlung wie AssertionError
Vorlage für Programm● Nutzt logging
● Nutzt Parser für Befehlszeilenoptionen
● Vom Anwender behebbare Fehler über log.error()
● Vom Entwickler behebbare Fehler über log.exception()
● Setzt Exit Code 0 oder 1
def main(arguments=None): if arguments is None: arguments = sys.argv
# Exit code: 0=success, >0=error. exitCode = 1
# Process arguments. In case of errors, report them and exit. parser = optparse.OptionParser(usage='process some report') parser.add_option("-o", "--out", dest="targetPath", help="write report to FILE", metavar="FILE") options, others = parser.parse_args(arguments) if len(others) < 1: # Note: parser.error() raises SystemExit. parser.error('input files must be specified')
try: _process(options, others) exitCode = 0 # Success! except KeyboardInterrupt: _log.error('stopped as requested by user') except (DataError, EnvironmentError) as error: _log.error(error) except Exception as error: _log.exception(error) return exitCode
if __name__ == "__main__": logging.basicConfig(level=logging.INFO) sys.exit(main())
Von mir zuimplementieren
Durchführen des Hauptteils
exitCode = 1
…
try: _process(options, others) exitCode = 0 # Success! except KeyboardInterrupt: _log.error('stopped as requested by user') except (DataError, EnvironmentError) as error: _log.error(error) except Exception as error: _log.exception(error) return exitCode
Von mir zuimplementieren
Wann except verwenden? (1)
● Ganz „außen“ in __main__ bzw. main()● Bei GUI-Anwendungen: um abgeschlossene
Benutzeraktionen (action pattern)● Zum Umwandeln von Exceptions in DataError● Zum Umwandeln von Fehlern und gültige
Zustände→ z.B. bei LookupError einen Defaultwert verwenden
Wann except verwenden? (2)
● Insgesamt: selten und gezielt● Wenig Aufwand für Entwickler● Fehlerbehandlung i.d.R. Trivial:
1.Zusammenräumen (with, finally, ...)
2.Routine abbrechen (raise oder aufgetretene Exception delegieren)
3.Aufrufer entscheidet, was zu tun ist
● Vorteile: Leicht wartbarer, kompakter Code mit wenig Einrückebenen
Wann raise verwenden?
● Konsequente Namenskonventionen für Routinen:● Prozeduren: „mach etwas“
Beispiel: sort(liste) → sortiert Liste, ändert Original● Funktionen: „etwas“ gemäß dem gelieferten
Beispiel: sorted(liste) → liefert sortierte Kopie einer Liste, Original bleibt unverändert
● Falls nicht möglich, das beschriebene „etwas“ zu machen oder liefern: raise
● Damit klare und einfache Definition von Fehlerbedingungen: alles, was daran hindert, „etwas“ zu machen
Lösungsorientierte Fehlerbehandlung (1)
● gezielte Nutzung der vorhandene Python-Mechanismen
● Unterscheidung: Wer kann Fehler beheben?● Anwender zur Laufzeit: Daten, Umgebung
→ EnvironmentError, DataError● Entwickler während Umsetzung: Programm
→ Assertions und Rest● Zusammenräumen mit with, finally und
Context Manager (nicht mit __del__())
Lösungsorientierte Fehlerbehandlung (2)
● Fehlerbehandlung im Programm:● Mit if … raise neue Fehler erkennen● Mit raise bereits erkannte Fehler meist einfach
weiterleiten● An einigen wenigen stellen mit except abfangen
und Meldung ausgeben● Schema für gute Fehlermeldung:
→ beschreibt die Lösung statt den Fehler
cannot do <some task>:<something> is <actual> but must be <expected>