Debugging - Institut für Statistik

FoRt: Debugging
Fabian Scheipl Institut für Statistik LMU München
Debugging
Debugging
Jedes Programm hat Fehler.
Debugging, also Fehlersuche und -korrektur, ist ein wichtiger und unvermeidbarer Bestandteil von Programmierung. (. . . nicht zu schweigen von der Nutzung von ‘open source’ Software die großteils von
autodidaktischen Doktoranden unter Schlafentzug entwickelt wird)
Effektives und effizientes debugging ist essentiell für produktives Programmieren.
Ablauf
1. Beschreibung:
• was läuft schief?
• unter welchen Umständen / Randbedingungen?
2. Verortung:
• welche Zeilen lösen den Fehler aus?
• welche Zeilen verursachen den Fehler?
3. Berichtigung des Fehlers & Überprüfung der Programmänderungen:
• Fehler durch Änderung behoben?
• sonstiges Verhalten des Programms unverändert?
Rationales & Rationelles Debugging
modus operandi: We do SCIENCE!
Rationales & Rationelles Debugging
Auf repeat:
1.
2.
3.
4.
formuliere Hypothese: “Bug wird verursacht von XY.”
designe & führe Experiment aus um Hypothese zu prüfen
analysiere Ergebnis
immer noch fehlerhaft? zurück auf 1.
Debugging
Herausfinden
• was kaputt ist
• wo
indem sukzessive mögliche Fehlerquellen eliminiert werden.
1
Rationales & rationelles Debugging
Leitfragen:
• Was für ein Verhalten erwarte ich?
• Was für ein Verhalten beobachte ich?
• Was ist der Unterschied?
Dann
• Hypothesenbildung (“Woran liegt’s?”)
• und Bugfix (“Was muss ich ändern?”).
Beschreibung
1. Fehlermeldung reproduzierbar machen
• unter welchen Umständen tritt Fehler auf?
• . . . auch in einem sauberen workspace?
2. Fehler eingrenzen
• wie stark können sich die Inputs ändern ohne dass der Fehler verschwindet?
• produzieren geänderte Inputs andere Fehler?
• für welche Inputs verschwindet der Fehler?
Verortung
Immer ein guter erster Schritt:
traceback(): Welche Funktionsaufrufe haben den Fehler ausgelöst?
(setze options(deparse.max.lines=5) um Output lesbar zu halten)
traceback()
f <- function(a)
g <- function(b)
h <- function(c)
i <- function(d)
f(10)
g(a)
h(b)
i(c)
"a" + d
Error in "a" + d : non-numeric argument to binary operator
traceback()
4:
3:
2:
1:
i(c) at traceback-1.R#3
h(b) at traceback-1.R#2
g(a) at traceback-1.R#1
f(10) at traceback-1.R#5
2
traceback()
4:
3:
2:
1:
i(c) at traceback-1.R#3
h(b) at traceback-1.R#2
g(a) at traceback-1.R#1
f(10) at traceback-1.R#5
• Kette der calling environments
• Code der per source() geladen wurde mit Dateiname#Zeilennummer
Verortung
andere Werkzeuge:
• options(error=recover): springt in den interaktiven Debugger direkt bevor der Fehler ausgelöst wird.
Höchst empfehlenswert.
• debug(<function>), debugonce(<function>): interaktiver Debugger öffnet sich sobald die Funktion
aufgerufen wird. undebug(<function>) um debug mode wieder zu deaktivieren.
• in eigenen Skripten: Einfügen von browser()-statements. (in RStudio: Breakpoint setzen direkt im
Editor)
• eigener oder fremder (package) Code: trace() mit edit=TRUE, dann Einfügen von browser, print or
cat, z.B.
Verortung
• nicht empfehlenswert: Code mit vielenvielen print, cat, etc. Befehlen “verzieren” um z.B Variablenwerte
nachzuverfolgen
• error=recover()-option abschalten mit
# default wiederherstellen:
options(error=NULL)
# oder:
options(error=traceback)
Interaktiver Debugger
• interaktive Konsole innerhalb der Funktion
• führe beliebigen R Code in der Konsole im evaluation frame des Funktionsaufrufes aus.
debug(f)
f(10)
debugging in: f(10)
debug: g(a)
Browse[2]>
Interaktiver Debugger
3
options(error=recover)
f(10)
Error in "a" + d : non-numeric argument to binary operator
Enter a frame number, or 0 to exit
1:
2:
3:
4:
f(10)
traceback-1.R#1: g(a)
traceback-1.R#2: h(b)
traceback-1.R#3: i(c)
Selection:
Interaktiver Debugger: Spezielle Befehle
• n, <enter>: führe nächsten Befehl der Funktion aus. print(n) um lokale Variable n auszugeben, falls
vorhanden.
• c: führe den gesamten aktuellen Codeblock aus:
– nützlich innerhalb von Schleifen: kein manuelles Steppen durch jede einzelne Iteration
– nützlich um zu überprüfen ob Funktion komplett & korrekt durchläuft nachdem unerwünschte
Variablenwerte manuell im Debugger korrigiert wurden
• Q: stoppt den Debugger, beendet Funktionsauswertung und geht zurück in die normale Konsole.
• sinnvoller erster Schritt im Debugger: ls.str() für Übersicht über lokale Variablen und ihre Werte
Debugging workflow
•
•
•
•
lies die Fehlermeldung. Echt jetzt.
lies die Fehlermeldung nochmal. Denk drüber nach.
noch kein Plan? Rufe traceback()
immer noch kein Plan? Lies die Hilfe der Funktion die den Fehler ausgibt.
Debugging workflow
Immer noch kein Plan? Springe an die Stelle im Code die den Fehler auslöst um genauer nachzusehen:
•
•
•
•
•
setze options(error=recover, deparse.max.lines=5)
lass den Code der den Fehler getriggert hat nochmal laufen
wähle geeigneten frame (nicht zu niedrig im call stack, nicht zu hoch. . . )
schau den Zustand der lokalen Variablen bei Auslösung des Fehlers an: ls.str()
lies den Quellcode der Funktion um zu verstehen was der Zustand der lokalen Variablen an dieser Stelle
im Programm sein sollte
• hilft alles nüscht? Fang von vorne an, wähle anderen frame aus dem call stack
4
Debugging: Eigener Code in RStudio
Debugging: Code in (CRAN) Paketen
Wie komme ich an den Quellcode von Paketen?
1. Gebe Namen der Funktion in die Konsole ein (Keine Kommentare, nicht so ganz einfach für S3/S4
Methoden und nicht-exportierte Funktionen.)
2. Lade <PAKET>.tar.gz von CRAN/R-forge/Bioconductor/etc. herunter, extrahiere lokal und durchsuche
Dateien im /R-Verzeichnis
3. Gehe auf https://github.com/cran/<PAKET>, benutze Github-Suchfunktion
Untersuchung unerwarteter Warnungen
• konvertiere Warnungen in Fehler mit options(warn = 2)
• benutze übliche debugging Werkzeuge (traceback, options(error=recover), . . . )
Modifikation
Problem verstanden und verortet? Modifizierungen vorgenommen?
Dann Validierung:
• ursprüngliche(r) Fehler tatsächlich behoben?
• modifizierter Code läuft fehlerfrei für restliche Tests durch und liefert korrekte Ergebnisse?
Validierung nie vergessen, sonst häufig “Verschlimmbesserung” durch Bugfixes.
Um Hilfe bitten
Externe Hilfe bekommen: MREs
Minimale, reproduzierbare Beispiele (minimal reproducible example (MRE)) ermöglichen es gefundene
Probleme präzise an andere zu kommunizieren.
Sie enthalten:
• minimalen Datensatz der nötig ist um den Fehler zu reproduzieren
• minimal kurzer Code der nötig ist um den Fehler zu reproduzieren und der in einem leeren Workspace
mit dem gegebenen Datensatz läuft
• der Output von sessionInfo() in der R-Session in der der Fehler aufgetreten ist
• bei Code der Zufallsgeneratoren aufruft: ein RNG seed (also set.seed()) um Beispiel reproduzierbar
zu machen
Externe Hilfe bekommen: Web
•
•
•
•
StackOverflow
R Help Mailingliste [email protected] (wenn man gerne angeraunzt wird. . . )
Bug in einem Paket: kontaktiere maintainer & schicke ihr/ihm ein minimales, reproduzierbares Beispiel
Paket auf GitHub: öffne ein issue im entsprechenden repository
5
Beliebte & Bekannte Fehler
Häufige Fehlerquellen: Syntax
•
•
•
•
[[ vs [
== vs =
== vs all.equal, Gleitkommazahlen: 1e-380 == 0
vector vs matrix vs array
– benutze [ mit drop=FALSE
– schreibe vektorisierten Code
• implizite Typenumwandlung: (z.B. string zu factor)
Häufige Fehlerquellen: Syntax
R-Spezialität: argument recycling
x <- 1:3
x[1:4]
## [1]
1
2
3 NA
matrix(x, 2, 2)
## Warning in matrix(x, 2, 2): data length [3] is not a sub-multiple or
## multiple of the number of rows [2]
##
[,1] [,2]
## [1,]
1
3
## [2,]
2
1
Häufige Fehlerquellen: Syntax
Vermeidung & Verteidigung:
• input checking: validiere Funktionsargumente (stopifnot(...), if(...) stop(...), if(missing(...))
• benutze Befehle und Funktionen die immer die selbe Art von Rückgabewert produzieren:
– vapply statt sapply
– [ mit drop=FALSE
Häufige Fehlerquellen: Problem zwischen Stuhl und Computer
•
•
•
•
falsche / ungeeignete inputs
verwirrende / uneinheitliche Benamung
unbenamte Funktionsargumente in der falschen Reihenfolge
Übertragungsfehler: Gehirn / Papier → Code
6
Häufige Fehlerquellen: Scoping
• undefinierte / falsch definierte globale Variablen
• nach Kapselung von Funktionen: Nebeneffekte auf die der Code sich implizit gestützt hatte nicht mehr
vorhanden (gut so. . . .)
Easy Debugging
Debugging is twice as hard as writing the code in the first place. Therefore, if you write the code
as cleverly as possible, you are, by definition, not smart enough to debug it.
— Brian Kernighan (UNIX co-creator)
Easy Debugging
Bugs sind unvermeidbar.
Debugging ist oft frustrierend und immer zeitraubend.
Code der leicht zu debuggen ist ist meistens:
• weniger elegant
• weniger knapp
• schwieriger zu schreiben
. . . das ist es trotzdem wert!
Leicht zu debuggender Code
• . . . hat viele Kommentare:
– jede Funktion mit mindestens 3 Kommentarzeilen: Zweck, inputs, outputs
– schwierige, knifflige Abschnitte: warum & wie, nicht nur was
– klare Zuschreibung von fremdem Code: wann & von wo (URL) kopiert? was modifiziert?
• Benamung:
– Abkürzungen vermeiden
– aussagekräftige, eindeutige Namen
– konsistentes Schema
Leicht zu debuggender Code
•
•
•
•
modularer, funktionaler Code (top-down design)
“reine” Funktionen
don’t repeat yourself!: Kapselung
teste früh, teste oft, teste alles
7
Modularer & funktionaler Code
. . . macht Fehler leichter lokalisierbar:
• “low-level” Funktion OK? → Bug auf höherem Abstraktionslevel
• Funktion OK für künstliche inputs? → falsche / unerwartete inputs oder Fehler in einer Funktion auf
niedrigerem Level
Reine Funktionen
Respect the Interface!
Kapselung & Abtrennung von Unterfunktionen sind hilfreich:
• benutze nur Variablen die als Argumente übergeben wurden
– keine versteckten Abhängigkeiten!
– zB von globalen Variablen in Elternumgebungen des execution frame o.ä.
• Benutzung von Variablen die in der closure der Funktion liegen kann sehr eleganten und knappen Code
produzieren, ist aber meist schwer zu durchschauen und zu debuggen
• <<- ist pfuibäh!
8