2.41. Debugging¶
Die meisten Skripte, die auf der Kamera fehlschlagen, tun dies auf eine von drei Arten: Sie lösen eine Ausnahme aus, sie liefern den falschen Wert oder sie hängen sich auf. Für jeden Fall gibt es einen eigenen Satz an Werkzeugen.
2.41.1. Einen Traceback lesen¶
Wenn ein Skript eine Ausnahme auslöst und nichts diese behandelt, gibt die REPL oder die IDE einen Traceback aus – eine Aufzeichnung der Aufrufkette vom äußersten Skript bis hinunter zu der Zeile, die die Ausnahme ausgelöst hat.
Ein Traceback wird von unten nach oben gelesen:
Die unterste Zeile nennt die Ausnahmeklasse und ihre Meldung (
ValueError: invalid literal for int()...).Jeder
File "...", line N, in <name>-Block darüber ist ein Frame – je weiter oben, desto tiefer im Aufruf.Der oberste Frame ist die Stelle, an der das Skript begann; der unterste Frame ist die Stelle, an der der Fehler ausgelöst wurde.
Lesen Sie zuerst die unterste Zeile, um zu erfahren, was schiefgelaufen ist, und arbeiten Sie sich dann nach oben vor, um zu sehen, wie das Skript dorthin gelangt ist. Die Zeilennummern verweisen auf die genauen Quellpositionen im Skript.
2.41.2. Print-Debugging¶
Der schnellste Weg, herauszufinden, was ein Skript tut, besteht darin, die verdächtigen Werte auszugeben. Drei eingebaute Funktionen machen Ausgaben nützlicher:
repr()– gibt die entwicklerorientierte Zeichenkettendarstellung eines Werts zurück.print(repr(value))unterscheidet"5"von5undNonevon"None", was ein einfachesprint()nicht leisten kann.type()– gibt die Klasse eines Werts zurück. Mitprint(type(value))findet man heraus, ob die Variable, die „eigentlich ein int sein sollte“, heimlich eine Zeichenkette ist.len()– die Länge einer Sequenz oder Sammlung. Ein überraschend großer Anteil der Fehler sind Off-by-One- oder Größenungleichheitsprobleme.
print("got:", repr(value), "type:", type(value), "len:", len(value))
Setzen Sie in jeden Zweig, der Sie interessiert, eine print-Anweisung – in beide Arme eines if, in jeden except-Block, in den Rumpf einer Schleife, von der Sie vermuten, dass sie null Mal durchlaufen wird. Der Preis ist eine Zeile Ausgabe; der Nutzen besteht darin, herauszufinden, ob der Codepfad, von dem Sie denken, dass er ausgeführt wird, auch tatsächlich ausgeführt wird.
2.41.3. Ein Objekt erkunden¶
Zwei eingebaute Funktionen beantworten die Frage „was kann ich mit diesem Ding tun“:
dir()– gibt eine Liste aller auf einem Objekt definierten Namen zurück: Methoden, Attribute, Dunder, alles.help()– gibt den Docstring (und auf CPython die Signatur) einer Funktion, Methode oder Klasse aus.
Verwenden Sie sie zusammen: dir findet den Namen, help erklärt, was er tut.
2.41.3.1. Einen Namen mit dir finden¶
>>> dir([1, 2, 3])
['__add__', '__class__', '__contains__', '__delitem__',
'__eq__', '__ge__', ..., 'append', 'clear', 'copy',
'count', 'extend', 'index', 'insert', 'pop', 'remove',
'reverse', 'sort']
Der erste Teil der Liste sind Dunder-Methoden, die von jedem Objekt geerbt werden; die Namen, nach denen es sich zu suchen lohnt, stehen meist danach. dir funktioniert auf allem – einer Klasse, einer Instanz, einem Modul, einem eingebauten Typ:
>>> import json
>>> dir(json)
['__name__', 'dump', 'dumps', 'load', 'loads']
Diese zweite Form ist der Weg, um herauszufinden, welche Namen der obersten Ebene ein Modul tatsächlich bereitstellt, ohne die REPL zu verlassen.
2.41.3.2. Mit help nachschlagen¶
Sobald dir einen Kandidaten zutage gefördert hat, beschreibt ihn help:
>>> help(str.split)
split(sep=None, maxsplit=-1)
Return a list of the words in the string, ...
Auf MicroPython ist help schlanker als auf CPython – manchmal nur die Signatur, manchmal ein einzeiliger Docstring, manchmal gar nichts für eingebaute C-Funktionen. Es bleibt dennoch eine schnelle Gedächtnisstütze, wenn der IDE-Tooltip nicht zur Hand ist.
2.41.4. Wenn sich etwas aufhängt¶
Ein Skript, das nicht zurückkehrt, ist schwerer zu diagnostizieren als eines, das eine Ausnahme auslöst. Häufige Verursacher:
Eine
while-Schleife, deren Bedingung niemals falsch wird. Fügen Sie in jeder Iteration eine Ausgabe der Schleifenvariablen hinzu; ändert sich der Wert nicht, hat der Schleifenrumpf einen Fehler.Ein blockierender Aufruf, der auf eine Eingabe wartet, die nie eintrifft – ein Lesen aus einer leeren Warteschlange, ein sleep ohne Ende. Klammern Sie den Aufruf mit Ausgaben ein, um zu sehen, an welcher Zeile das Skript festhängt.
Eine unendliche Rekursion. Der Traceback, wenn sie schließlich ausgelöst wird (mit
RecursionError), weist meist direkt darauf hin.
Die wirksamste Methode zur Wiederherstellung eines hängenden Skripts ist die Stop-Schaltfläche der IDE, die einen KeyboardInterrupt über USB an das Skript sendet. Der Interrupt erscheint als Traceback an der gerade laufenden Zeile – oft genau die Zeile, die nicht zurückkehrt.
Bemerkung
Wenn sich ein Aufhängen jeder Diagnose widersetzt – das Skript erscheint korrekt, der Interrupt-Traceback verweist auf eine eingebaute Funktion oder auf Firmware-Code statt auf Ihr Skript, oder derselbe Code funktionierte mit einem früheren Firmware-Build – könnte die Ursache eher ein Firmware-Fehler als ein Skriptfehler sein. Reduzieren Sie das Skript auf den kleinsten Reproduzierer, der immer noch hängt, und eröffnen Sie einen Bericht im OpenMV-Forum. Geben Sie die Firmware-Version, das Board, auf dem es lief, und das reduzierte Skript an.
2.41.5. Entfernen Sie die Diagnose vor der Auslieferung¶
Strategische Ausgaben während der Entwicklung sind großartig; hundert print-Aufrufe, die in einem Produktionsskript verbleiben, überladen die Ausgabe und belegen Heap, den die eigentliche Arbeit stattdessen nutzen könnte. Wenn ein Fehler behoben ist, entfernen Sie die Ausgaben (oder schützen Sie sie hinter einem Debug-Flag, das Sie ausschalten können).
Für Diagnosen, die langfristig im Codepfad verbleiben sollen, wechseln Sie von print() zum Modul logging. Es ordnet jeder Meldung ein Level zu (debug, info, warning, error) und ermöglicht es, mit einer einzigen Einstellung die leisen Meldungen in der Produktion verstummen zu lassen:
import logging
log = logging.getLogger("main")
log.info("starting up")
log.debug("loaded config: %s", config)
log.warning("falling back to defaults")
Das Setzen des Logger-Levels auf logging.WARNING lässt die info- und debug-Aufrufe im Wesentlichen nichts kosten (die Meldungszeichenkette wird nie aufgebaut), ohne dass Zeilen auskommentiert werden müssen. Das macht logging zum richtigen Werkzeug für dauerhafte Diagnosen; ein rohes print ist für wegwerfbare in Ordnung.