Diese Präsentation wurde erfolgreich gemeldet.
Wir verwenden Ihre LinkedIn Profilangaben und Informationen zu Ihren Aktivitäten, um Anzeigen zu personalisieren und Ihnen relevantere Inhalte anzuzeigen. Sie können Ihre Anzeigeneinstellungen jederzeit ändern.
Lösungsorientierte
Fehlerbehandlung
Thomas Aglassinger
http://www.roskakori.at
https://github.com/roskakori/talks
Agenda
1.Begriffserklärung: Fehler
2.Darstellung von Fehler in Python
3.Lösungsorientierter Ansatz
4.Fehlermeldungen
5.Ähn...
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...
Fehler in Python
Wozu Beispiele in Python?
● Leicht verständlich und gut lesbar
● Kompakter Code
● Weitgehend englische Sätze
● Prozedural ...
Fehler in Python
● Darstellung über Exceptions
● Durchgängige Verwendung in Standard-Bibliothek
● Vorteil: nicht unabsicht...
Fehler erkennen
● Erkennen mit if und einer Fehlerbedingung
● Aufzeigen mit raise und einer Fehlermeldung
● Fehler führt z...
Fehler abfangen und ausgeben
● Aufruf des möglicherweise fehlschlagenden Codes mit try
● Bestimmte Fehler abfangen mit exc...
Ressourcen immer freigeben (1)
● Mit finally: sowohl bei Erfolg als auch Fehler
'some.txt', 'rb')
processData(inputFile)
f...
Ressourcen immer freigeben (2)
● Mit with-Statement:
as inputFile:
● Voraussetzung: verwendete Klasse implementiert
Contex...
Ressourcen immer freigeben (3a)
● Mit eigenem Context Manager
● Schritt 1: Definition von __enter__() und __exit__()
class...
Ressourcen immer freigeben (3b)
● Mit eigenem Context Manager
● Schritt 2: Aufruf wie zuvor über with-Statement
'www.pytho...
Ressourcen immer freigeben (4b)
● Mit eigenem Context Manager
● Schritt 2: Aufruf wie zuvor über with-Statement
with Socke...
Ressourcen freigeben (5)
● Nicht verwenden: __del__()
● Aufruf erfolgt durch Garbage Collector
● Nicht vorhersagbar wann →...
Fehler erkennen mit assert (1)
● Beispiel von zuvor
def processSomething(height):
if height <= 0:
raise ValueError(
'heigh...
Fehler erkennen mit assert (2)
● Wenn Bedingung verletzt:
wie raise AssertionError('...')
● Deaktivieren von assert mittel...
Zusammenfassung
● Fehler erkennen mit raise und assert
● Fehler abfangen mit try und except
● Aufräumarbeiten: finally, wi...
Lösungsorienterter Ansatz
zur Fehlerbehandlung
Grundprinzipen
● Im Zentrum der Überlegungen steht die
Beseitigung des Fehlers (Lösung) und nicht
der Fehler selbst
● Klar...
Zuständigkeiten
● Entwickler: Umsetzung des Programms zur
● Verarbeitung der Daten und Eingaben des
Anwenders
● Liefern de...
Nutzung von assert
● Fehlererkennung: aus internen Programmzustand
● Lösung: Änderung des Programms
● Zielgruppe für Fehle...
Nutzung von raise
● Fehlererkennung: aus Daten und in Umgebung
● Lösung:
● Daten: korrekte und vollständige Eingabe
● Umge...
Fehlermeldungen
Anforderungen
● In Literatur oft: unklare Richtlinien („hilfreich“,
„verständlich“, ...)
● In Praxis oft: Beschreibung, wa...
Ableiten der Fehlermeldung aus
Programmcode
● Allgemein:
if height <= 0:
raise ValueError(
'height is %d but must be great...
Darstellung des Zusammenhangs
● Bei raise: Anführen von Name und Wert des
Ist-Zustands (z.B. „height is -3“)
● Bei except:...
Beispiel: Fehler erkennende Routine
def processSomething(height):
if height <= 0:
raise ValueError('height must be greater...
Beispiel: Fehler berichtender Code
def processAllThings(dataFile):
try:
# Process all heights read from `dataFile`.
lineNu...
Wo Fehlermeldung ausgeben?
● Bei GUI oder Web-Anwendung:
● Bei Eingaben: Feld hervorheben und Meldung unter
dem betroffene...
Ausgabe in Log-Datei
● Mit Standard Modul logging:
http://docs.python.org/2/library/logging.html
● Mehrere Stufen zur Bewe...
Logging auf stderr
● Auch für Befehlzeilenanwendungen nutzbar
import
def processData(dataPath):
_log.info(u'read "%s"', da...
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", lin...
Logging auf stderr
● Kein eigener Code für Fehlerbehandlung
→ kein Aufwand für Entwickler
● Auch im Fehlerfalle Schließen ...
Lösungsorientierte Nutzung
von Exception-Hierarchien
Exception Hierarchie
● Exceptions sind Klassen
http://docs.python.org/2/library/exceptions#exception-hierarchy
● Gruppieru...
Lösungsorientiere Nutzung
● Vom Entwickler lösbar: AssertionError
● Vom Anwender lösbar: EnvironmentError
→ Dateien, Netzw...
Alle anderen vom Anwender
behebaren Fehler
● Mit except abfangen und umwandeln in
eigene Exception, die klar als vom Anwen...
Beispiel DataError
● Für fehlerhafte Daten aus Eingangsdatei:
class DataError(Exception):
pass
raise DataError('height is ...
Umwandeln einer Exception in
DataError
Fehler abfangen und Meldung übernehmen:
try:
# Process all heights read from `dataF...
Umwandeln einer Exception in
DataError
● In Python 3: Stack Trace erhalten mit Exception Chaining:
try:
# Process all heig...
Alle anderen vom Entwickler
behebaren Fehler
● Sind nun über „alles andere aber kein
DataError“ erkennbar
● Behandlung wie...
Programmvorlage
Vorlage für Programm
● Nutzt logging
● Nutzt Parser für
Befehlszeilenoptionen
● Vom Anwender
behebbare Fehler über
log.err...
Durchführen des Hauptteils
exitCode = 1
…
try:
_process(options, others)
exitCode = 0 # Success!
except KeyboardInterrupt:...
Empfehlungen für
except und raise
Wann except verwenden? (1)
● Ganz „außen“ in __main__ bzw. main()
● Bei GUI-Anwendungen: um abgeschlossene
Benutzeraktione...
Wann except verwenden? (2)
● Insgesamt: selten und gezielt
● Wenig Aufwand für Entwickler
● Fehlerbehandlung i.d.R. Trivia...
Wann raise verwenden?
● Konsequente Namenskonventionen für Routinen:
● Prozeduren: „mach etwas“
Beispiel: sort(liste) → so...
Zusammenfassung
Lösungsorientierte
Fehlerbehandlung (1)
● gezielte Nutzung der vorhandene Python-
Mechanismen
● Unterscheidung: Wer kann F...
Lösungsorientierte
Fehlerbehandlung (2)
● Fehlerbehandlung im Programm:
● Mit if … raise neue Fehler erkennen
● Mit raise ...
Nächste SlideShare
Wird geladen in …5
×

Lösungsorientierte Fehlerbehandlung

264 Aufrufe

Veröffentlicht am

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

Veröffentlicht in: Technologie
  • Als Erste(r) kommentieren

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

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>

×