Lösungsorientierte Fehlerbehandlung

172 Aufrufe

Veröffentlicht am

Richtlinien zur einfache n Umsetzung von Programmcode zur Fehlerbehandlung am Beispiel von Python.

Veröffentlicht in: Technologie
0 Kommentare
0 Gefällt mir
Statistik
Notizen
  • Als Erste(r) kommentieren

  • Gehören Sie zu den Ersten, denen das gefällt!

Keine Downloads
Aufrufe
Aufrufe insgesamt
172
Auf SlideShare
0
Aus Einbettungen
0
Anzahl an Einbettungen
5
Aktionen
Geteilt
0
Downloads
0
Kommentare
0
Gefällt mir
0
Einbettungen 0
Keine Einbettungen

Keine Notizen für die Folie

Lösungsorientierte Fehlerbehandlung

  1. 1. Lösungsorientierte Fehlerbehandlung Thomas Aglassinger http://www.roskakori.at https://github.com/roskakori/talks
  2. 2. 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
  3. 3. Begriffserklärung: Fehler
  4. 4. Was ist ein Fehler? Ein Fehler ist die Abweichung des Ist-Stands von der Erwartung
  5. 5. 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?
  6. 6. Fehler in Python
  7. 7. 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
  8. 8. 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)
  9. 9. 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
  10. 10. 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
  11. 11. Ressourcen immer freigeben (1) ● Mit finally: sowohl bei Erfolg als auch Fehler 'some.txt', 'rb') processData(inputFile) finally:
  12. 12. 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
  13. 13. 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()
  14. 14. 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.0n' + 'Host: www.python.orgn' +'n') reply = pythonOrg.clientSocket.recv(64) print(reply)
  15. 15. 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.0n' + 'Host: www.python.orgn' +'n') reply = pythonOrg.clientSocket.recv(64) print(reply) Ergebnis von __enter__(). Aufruf von __exit__(). Aufruf von __init__().
  16. 16. 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()
  17. 17. 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:
  18. 18. 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
  19. 19. Zusammenfassung ● Fehler erkennen mit raise und assert ● Fehler abfangen mit try und except ● Aufräumarbeiten: finally, with und Context Manager ● Nicht verwenden: __del__()
  20. 20. Lösungsorienterter Ansatz zur Fehlerbehandlung
  21. 21. 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
  22. 22. 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)
  23. 23. 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
  24. 24. 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
  25. 25. Fehlermeldungen
  26. 26. 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
  27. 27. 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>')
  28. 28. 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
  29. 29. 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
  30. 30. 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
  31. 31. 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
  32. 32. 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, ...
  33. 33. 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__':
  34. 34. Logging auf stderr ● Was passiert im angeführten Beispiel, wenn die Datei data.txt nicht auffindbar ist?
  35. 35. 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
  36. 36. 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
  37. 37. Lösungsorientierte Nutzung von Exception-Hierarchien
  38. 38. Exception Hierarchie ● Exceptions sind Klassen http://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
  39. 39. 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
  40. 40. 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
  41. 41. Beispiel DataError ● Für fehlerhafte Daten aus Eingangsdatei: class DataError(Exception): pass raise DataError('height is %d but must be greater than 0' % height)
  42. 42. 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))
  43. 43. 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
  44. 44. Alle anderen vom Entwickler behebaren Fehler ● Sind nun über „alles andere aber kein DataError“ erkennbar ● Behandlung wie AssertionError
  45. 45. Programmvorlage
  46. 46. 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 zu implementieren
  47. 47. 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 zu implementieren
  48. 48. Empfehlungen für except und raise
  49. 49. 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
  50. 50. 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
  51. 51. 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
  52. 52. Zusammenfassung
  53. 53. 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__())
  54. 54. 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>

×