54
Lösungsorientierte Fehlerbehandlung Thomas Aglassinger http://www.roskakori.at https://github.com/roskakori/talks

Lösungsorientierte Fehlerbehandlung

Embed Size (px)

Citation preview

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

Begriffserklärung: Fehler

Was ist ein Fehler?

Ein Fehler ist die

Abweichung des Ist-Stands von der Erwartung

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?

Fehler in Python

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__()

Lösungsorienterter Ansatzzur Fehlerbehandlung

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

Fehlermeldungen

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

Lösungsorientierte Nutzungvon Exception-Hierarchien

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

Programmvorlage

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

Empfehlungen fürexcept und raise

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

Zusammenfassung

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>