Blog

Debuggen in R: So können Sie Fehler in Ihrem Code einfach und effizient beheben

Wenn Sie Code schreiben, ist es unvermeidlich, dass sich von Zeit zu Zeit mal ein Fehler einschleicht. Debuggen bedeutet, den Fehler in Ihrem Code zu lokalisieren (wo?), identifizieren (was?) und den Grund für das Auftreten (warum?) herauszufinden, um den Fehler beseitigen zu können. Typischerweise verfolgt man beim Debuggen den folgenden Ablauf:

  1. Code ausführen,
  2. Stoppen des Codes, wenn etwas Verdächtiges passiert,
  3. Schrittweise Betrachten des Codes, um entweder die Werte einiger Variablen zu ändern oder den Code selbst zu ändern.

Da R eine Interpretersprache ist, bedeutet das Debuggen in R grundsätzlich, dass die Untersuchungseinheit Funktionen sind.

Es gibt einige Arten von Fehlern, auf die man in R stoßen kann und wird:

  • messages geben dem Benutzer einen Hinweis darauf, dass etwas nicht stimmt oder möglicherweise fehlt. Sie können mit suppressMessages() ignoriert oder ganz unterdrückt werden.
  • warnings stoppen nicht die Ausführung einer Funktion, sondern geben einen Hinweis darauf, dass etwas Ungewöhnliches passiert. Sie zeigen mögliche Probleme an.
  • errors sind schwerwiegende Probleme, die dazu führen, dass die Ausführung vollständig angehalten wird. Fehler werden verwendet bzw. zurückgegeben, wenn die Funktion ihre Aufgabe nicht fortsetzen kann.

Diese Probleme können auf verschiedene Weise angegangen werden. Beispielsweise kann die Fehlerbehandlung proaktiv gesteuert werden, indem verschiedene Fehlerquellen mittels Tools wie try(), tryCatch(), und withCallingHandlers() abgefangen werden und damit der Code robust gemacht wird.

R bietet auch mehrere fortgeschrittene Debugging-Tools, die sehr hilfreich sein können, um Probleme schnell und effizient zu lokalisieren. Dies wird der Schwerpunkt dieses Artikels sein. Zur Veranschaulichung verwenden wir ein Beispiel aus einem hervorragenden Paper von Roger D. Peng, und zeigen, wie diese Tools funktionieren sowie wie man diese Tools (aktuell) in RStudio nutzen kann. Die Debbuging-Tools können im Übrigen nicht nur zum Beheben von Fehlern, sondern auch für Warnings verwendet werden, indem man diese über options(warn = 2) zu Fehlern konvertiert.

traceback()

Wenn wir unseren Code ausgeführt haben und die Ausführung bereits aufgrund eines Fehlers abgebrochen ist, können wir mithilfe von traceback() versuchen, herauszufinden, wo dies passiert ist. traceback() gibt dazu eine Liste der Funktionen aus, die vor dem Auftreten des Fehlers aufgerufen wurden, den sogenannten "Call Stack". Der Call Stack wird von unten nach oben gelesen:

traceback.png

traceback() zeigt an, dass der Fehler bei der Ausführung von func3(y) aufgetreten ist.

Eine andere Möglichkeit, traceback() zu verwenden, besteht darin, traceback() routinemäßig als Fehlerbehandlung zu verwenden (d.h., es wird sofort aufgerufen, wenn ein Fehler auftritt). Dies kann auch in den Options festgelegt werden: options(error = traceback).

Alternativ kann traceback() auch direkt über die Schaltfläche auf der rechten Seite der Fehlermeldung in RStudio aufgerufen werden:

showTraceback.png

Debug-Modus

Obwohl traceback() sehr nützlich ist, zeigt es uns nicht, wo genau ein Fehler in einer Funktion aufgetreten ist. Dafür brauchen wir den "Debug-Modus".

Wenn Sie in den Debug-Modus wechseln, wird Ihre Funktion angehalten und Sie können die Umgebung der Funktion selbst untersuchen und mit ihr interagieren. In der Laufzeitumgebung der Funktion können Sie einige nützliche neue Funktionen ausführen. Im Umgebungsfenster werden beispielsweise die Objekte angezeigt, die in der lokalen Umgebung der Funktion gespeichert sind. Sie können diese Objekte überprüfen, indem Sie ihren Namen in die Eingabeaufforderung des Browsers eingeben.

Sie können auch Code ausführen und die Ergebnisse anzeigen, die normalerweise nur die Funktion sehen würde. Änderungen können nicht nur angezeigt, sondern auch direkt im Debug-Modus vorgenommen werden.

debugModeAnnot.png

Sie werden feststellen, dass während des Debuggens die Eingabeaufforderung wechselt und nun Browse[1]> lautet. Dies informiert Sie darüber, dass Sie sich im Debug-Modus befinden. In diesem Modus haben Sie weiterhin Zugriff auf alle üblichen Befehle, aber auch auf einige zusätzliche. Diese können über die angezeigte Symbolleiste oder durch direkte Eingabe der Befehle in die Konsole verwendet werden:

  • ls() zeigt an welche Objekte in der aktuellen Umgebung verfügbar sind
  • str() und print() um diese Objekte zu untersuchen
  • n um das nächste Statement auszuführen
  • s um in der nächsten Zeile eine Ebene tiefer zu gelangen, wenn es sich um eine Funktion handelt. Von dort aus können Sie jede Zeile der Funktion durchgehen
  • where um einen Stack Trace aller aktiven Funktionsaufrufe auszugeben
  • f um die Ausführung der aktuellen Schleife oder Funktion zu beenden
  • c um den Debug-Modus zu verlassen und mit der regulären Ausführung der Funktion fortzufahren
  • Q um den Debug-Modus zu beenden, die Funktionsausführung zu schließen und zurück zur Eingabeaufforderung zu kehren

Der Debug-Modus klingt ziemlich nützlich, oder? Hier sind einige Möglichkeiten, wie wir darauf zugreifen können.

browser()

Eine Möglichkeit, in den Debug-Modus zu wechseln, besteht darin, die Funktion browser() manuell in den Code einzufügen, sodass Sie an einer ausgewählten Stelle in den Debug-Modus springen können.

browser.png

Wenn Sie browser() manuell für installierten Code (also z.B. eine Funktion aus einem CRAN Paket) verwenden möchten, können Sie den Code der Funktion mit print(functionName) ausgeben (oder den Quellcode lokal herunterladen) und browser() genauso verwenden, wie Sie es mit ihrem selbstgeschriebenen Code tun würden.

Sie müssen nichts Spezielles ausführen, um browser() zu beenden. Denken Sie jedoch daran, browser() wieder aus Ihrem Code zu entfernen, sobald Sie fertig sind.

debug()

Im Gegensatz zu browser(), das an einer beliebigen Stelle in Ihren Code eingefügt werden kann, fügt debug() am Anfang einer Funktion automatisch eine browser()-Anweisung ein.

debug.png

Dies kann auch erreicht werden, indem Sie in RStudio auf der rechten Seite der Fehlermeldung unter "Show Traceback" die Schaltfläche "Rerun with Debug" anklicken.

rerunDebug.png

Sobald Sie mit debug() fertig sind, müssen Sie undebug() aufrufen. Andernfalls wird der Debug-Modus jedes Mal aufgerufen, wenn die Funktion aufgerufen wird. Eine Alternative zu dieser Vorgehensweise ist die Verwendung von debugonce(). Mittels isdebugged() kann geprüft werden, ob sich eine Funktion im Debug-Modus befindet.

Optionen in RStudio

Zusätzlich zu debug() und browser(), können Sie den Debug-Modus auch aktivieren, indem Sie in RStudio Breakpoints im Editor festlegen, indem Sie in RStudio links neben die betreffende Zeile klicken oder indem Sie die Zeile auswählen und Umschalt + F9 eingeben. Breakpoints sind auf der linken Seite durch einen roten Kreis gekennzeichnet, der angibt, dass der Debug-Modus in dieser Zeile aktiviert wird, sobald der Code ausgeführt wird.

breakpoint.png

Breakpoints haben den Vorteil, dass der Code selbst nicht verändert muss, z.B. durch das Einfügen von browser(). Es ist jedoch wichtig zu wissen, dass Breakpoints in einigen Fällen nicht ordnungsgemäß funktionieren, und nicht bedingt verwendet werden können (im Gegensatz zu browser(), das z.B. in Verbindung mit if() verwendet werden kann).

Sie können RStudio auch veranlassen automatisch in den Debug-Modus zu wechseln. Beispielsweise kann RStudio die Ausführung anhalten, wenn ein Fehler auftaucht. Das kann folgendermaßen eingestellt werden: Debug (in der oberen Leiste) > On Error (Bei Fehler), dann ändern Sie "Error Inspector" in "Break in Code".

Um zu verhindern, dass der Debug-Modus bei jedem Auftreten eines Fehlers geöffnet wird, ruft RStudio den Debugger nur dann auf, wenn Ihr eigener Code betroffen ist. Wenn dies Probleme verursacht, navigieren Sie zu Options > General > Advanced > Erweitert und deaktivieren Sie die Option “Use debug error handler only when my code contains errors”.

Wenn Sie den Debug-Modus jedes Mal aufrufen möchten, wenn ein Fehler auftritt, verwenden Sie options(error = browser()).

recover()

recover() ähnelt browser(), Sie können jedoch auswählen, welche Funktion im Call Stack Sie debuggen möchten. recover() wird nicht direkt verwendet, sondern als Fehlerbehandlungsroutine durch das Setzen von options(error = recover).

Sobald ein Fehler aufgetreten ist, pausiert recover() R, gibt den Call Stack aus (wobei zu beachten ist, dass die Reihenfolge hier genau umgekehrt ist im Vergleich zu traceback()), und dann können Sie auswählen, für welche Funktion browser() aufgerufen werden soll. Dies ist hilfreich, da Sie jede Funktion im Call Stack durchsuchen können, sogar noch bevor der Fehler aufgetreten ist. Das ist nützlich, wenn die Ursache wenige Aufrufe vor dem tatsächlichen Auftreten des Fehlers liegt.

recover.png

Sobald Sie das Problem gefunden haben, können Sie zur Standard-Fehlerbehandlung zurückkehren, indem Sie die Option aus Ihrer .Rprofile-Datei entfernen. Beachten Sie, dass zuvor options(error = NULL) verwendet wurde, dies jedoch in R 3.6.0 illegal wurde und ab September 2019 dazu führen kann, dass RStudio abstürzt, wenn Sie das nächste Mal versuchen, beispielsweise .Rmd-Dateien auszuführen.

trace()

Die trace()-Funktion ist etwas komplizierter in der Verwendung, kann jedoch nützlich sein, wenn Sie keinen Zugriff auf den Quellcode haben (z. B. bei Basisfunktionen). Mit trace() können Sie beliebigen Code an einer beliebigen Stelle in eine Funktion einfügen, und die Funktionen werden nur indirekt geändert (ohne sie neu zu laden).

Die Syntax lautet wie folgt:

trace(what = yourFunction,
      tracer = some R expression, 
      at = code line)

Um herauszufinden, welche Zeile im Code verwendet werden soll, versuchen Sie: as.list(body(yourFunction))

Beachten Sie, dass trace(yourFunction) nur die Funktion selbst ausgibt, wenn es ohne zusätzliche Argumente (über den Namen der Funktion hinaus) aufgerufen wird:

trace4.png

Probieren wir es aus:

trace1.png

Jetzt ist unsere Funktion func3() ein Objekt mit Trace-Code:

trace1point5.png

Wenn wir den Tracing Code anzeigen möchten, um ein besseres Verständnis dafür zu bekommen, was vor sich geht, können wir body(yourFunction) verwenden:

trace2.png

Wenn wir hier die Funktion func1() aufrufen, wird der Debug-Modus geöffnet, wenn r keine Zahl ist.

trace3.png

Wenn Sie fertig sind, können Sie das Tracing mit untrace() von einer Funktion entfernen.

Und das war es! Diese Methoden mögen auf den ersten Blick etwas verwirrend erscheinen, aber sobald Sie den Dreh raus haben, werden diese wichtige Werkzeuge sein, mit denen Sie (leider unvermeidliche) Fehler in Ihrem Code schnell und effizient beheben können.