debugging en r

 

Introducción


Debuguear un código es el proceso de proceso de identificar y corregir errores dentro de él. R tiene varias maneras de indicar cuando algo falla en la ejecución de un fragmento de código, las cuales son importantes conocer para disminuir el tiempo que requiere esta tarea. Cuando ejecutamos cualquier función en R podemos obtener los siguiente resultados:

  • message: es una notificación genérica producida por al función message(). Luego de esta función, el código continua su ejecución.
  • warning: es una indicación que indica que algo anda mal, pero no fatalmente amal. Es producida por la función warning(), y luego de esta el código continua su ejecución.
  • error: es una indicación de que ocurrió un error fatal que detuvo la ejecución del código. Esta es producida por la función stop().
  • condition: es un concepto genérico par indica que algo inesperado ha ocurrido. Estas en general son creadas por los programadores para indicar lo que ellos desean.

Por ejemplo:

log(-1)
[1] NaN
Warning message:
In log(-1) : NaNs produced

a = b
Error: object ‘b’ not found

En el primer caso, como los logaritmos no están definidos para números negativos no obtenemos un resultado concreto. En cambio obtenemos un NaN (Not a Number) junto a un mensaje tipo warning que nos indica a que se debe esto. La razón de que R no detenga la ejecución del código luego de producir este evento es debido a considera que es posible continuar la ejecución asumiendo que el resultado de la operación es un NaN. En cambio, en el segundo caso, al intentar asignar a la variable a el valor de la variable b se produce un error fatal dado que esta última no está definida y no se puede asignar ningun valor a la variable a.

La principal tarea de debuguear cualquier código en R es diagnosticar correctamente cual es el problema en él. Cuando diagnosticamos un problema, es importante conocer primero cual es el resultado esperado de su ejecución. Luego se necesita identificar que ocurrió y como se desvió lo sucedido respecto de lo esperado. Algunos puntos para comenzar a investigar son:

  • Cuales y de que tipo son las entradas del código
  • Que esperamos que suceda cuando ejecutamos el código
  • Que obtenemos finalmente
  • Como difiere lo que obtenemos respecto de lo que esperábamos obtener

Finalmente, antes de pasar a ver las herramientas que nos provee R debemos mencionar que existen muchas técnicas para debuguear programas. Esto ya excede el objetivo de este post.

 

Herramientas para Debugging en R


R nos trae unas cuantas herramientas para ayudarnos en la tarea de debuguear código. Las herramientas principales son:

  • traceback: imprime en pantalla la secuencia de llamadas a funciones (function call stack) luego de que un error ocurre.
  • debug: marca una función en debug mode, el cual permite avanzar en la ejecución de esta por pasos, una linea por vez.
  • browser: suspende la ejecución de una función si es llamada y pone la función en debug mode.
  • trace: permite insertar código de depuración dentro de una función en un lugar especifico.
  • recover: permite modificar le comportamiento de un error, de modo que se pueda mirar el la pila de funciones llamadas.

Todas estas son herramientas interactivas específicamente diseñadas para facilitar la tarea de debuguear. También, junto a estas, es común utilizar la función print() para ir dejando constancia de posición/estado del código en determinadas posiciones que resulten de utilidad conocer.

 

traceback

La función traceback imprime en pantalla la pila de funciones llamadas luego de que un error fatal ocurra. Por ejemplo, si tenemos una función llamada a() la cual llama a otra función b() que a su vez llama a c() y así sucesivamente. Si un error ocurre, puede no quedar necesariamente claro en cual función el error ocurrió. Entonces, mediante traceback podemos explorar en cuantos niveles de profundidad el error ocurrió.

mean(x)
Error in mean(x) : object 'x' not found
traceback()
1: mean(x)

f1 <- function(n) mean(n)
f1(x)
Error in mean(n) : object 'x' not found
traceback()
2: mean(n) at #1
1: f1(x)

lm(y~x)
Error in eval(expr, envir, enclos) : object 'y' not found
traceback()
7: eval(expr, envir, enclos)
6: eval(predvars, data, env)
5: model.frame.default(formula = y ~ x, drop.unused.levels = TRUE)
4: stats::model.frame(formula = y ~ x, drop.unused.levels = TRUE)
3: eval(expr, envir, enclos)
2: eval(mf, parent.frame())
1: lm(y ~ x)

En el primer caso, es claro que el error ocurrió dentro de mean() porque el objeto x no está definido. La función traceback() debe ser llamada inmediatamente luego de que el error ocurra. Una vez que otra función es llamada, se pierde el rastro (traceback) del error.

En los siguientes casos, vemos como mediante traceback exploramos el stack de funciones llamadas. En un caso el error ocurre en el 2do nivel del stack, mientras que en el ultimo caso ocurre ocurre en el 7mo nivel.

Mirar la salida del traceback es util para descubrir donde ocurrió el error pero no es útil para obtener más información de este. Para tal caso debemos usar la función debug().

 

debug

La función debug() inicia un depurador interactivo (conocido como browser en R) parar una función. Con el depurador se puede ir paso a paso a través de la ejecución de una función hasta encontrar donde el error ocurre.

La función debug() toma como su primer argumento la función que queremos analizar. Por ejemplo:

f2 <- function(d)
{
  a = 1
  b = 2
  c = a + b
  mean(d)
}

debug(f2)

Ahora, cada vez que llamamos a f2() se ejecutará el modo interactivo de depuración. Para desactivar este comportamiento necesitamos ejecutar la función undebug().

f2(x)
debugging in: f2(x)
debug at #2: {
a = 1
b = 2
c = a + b
mean(d)
}
Browse[2]>

debug_mode

 

El depurador llama al browser al comienzo de la ejecución de la función. Desde allí se puede avanzar paso a paso en el cuerpo de la función. Para realizar esto hay una pequeña serie de comandos que debemos conocer:

  • n: ejecuta la expresion actual y avanza a la siguiente
  • c: continua la ejecucion de una función y no se detiene hasta que ocurra un error o se termine la ejecución de la función
  • Q: sale del browser

En el caso anterior:

Browse[2]> n
debug at #3: a = 1
Browse[2]> n
debug at #4: b = 2
Browse[2]> c
Error in mean(d) : object 'x' not found

Cuando estamos en el browser podemos ejecutar cualquier otra función de R. En particular, como ls() para ver en el espacio de trabajo en un determinado momento o la función print() para ver el valor de un objeto en algún determinado momento.

Para desactivar este modo debemos podemos usar undebug() para desactivarlos para todas las funciones, o pasar como argumento el nombre de la función para la cual queremos desactivarlo (por ejemplo undebug(f2)).

 

recover

La función recover() puede ser usada para modificar el comportamiento de R cuando un error ocurre. Normalmente, cuando un error ocurre en un función, R imprimirá en pantalla un mensaje de error, saldrá de la función y retornará al espacio de trabajo para esperar nuevos comandos.

Con recover() podemos decirle a R que cuando ocurra un error, este debe detener la ejecución en el punto exacto donde el error ocurrió. Esto nos da la oportunidad de mirar el espacio de trabajo cuando el error ocurrió, lo cual es útil para ver los valores de distintos objetos en el preciso momento de saltar el error.

options(error = recover)
f2(x)
Error in mean(d) : object 'x' not found

Enter a frame number, or 0 to exit

1: f2(x)
2: #6: mean(d)

Selection:

La función recover() imprimirá la pila de funciones cuando ocurra un error. En ese momento podremos elegir saltar a la función de interés e investigar el problema. Cuando elegimos una, nos enviará al browser y desde ahí miraremos.

 

Capturando excepciones


Más allá de los errores inesperados que tenemos que solucionar en forma interactiva cuando programamos, en otras ocasiones los errores con que nos topemos de alguna forma pueden ser “esperables” (también denominados “excepciones”). A estos errores conviene buscar la forma de gestionarlos automáticamente. Por ejemplo, un error esperable podría ser cuando intentamos leer el contenido de una archivo que no existe, pero de todas formas necesitamos que el código se siga ejecutando.

En R tenemos tres herramientas para manejar excepciones:

  • try: permite continuar la ejecución del código si se presenta un error.
  • tryCatch: permite ejecutar un fragmento de código especifico si se presenta un error.
  • withCallingHandlers: es una variante de tryCatch que establece manejadores (los fragmentos de código que se ejecutan cuando se captura una excepción) locales. Estos son raramente utilizados.

 

try

La funcion try permite ejecutar un fragmento de código dentro de ella y en caso de presentarse un error continuar con con la ejecución del programa. Por ejemplo, en el siguiente caso tenemos definida una función que evalúa el logaritmo de la variable de entrada. En caso de que la variable de entrada de la función no sea un numero se produce un error y el código se detiene.

f1 <- function(x) {
  log(x)
  "Hola"
}

f1("test")
Error in log(x) : non-numeric argument to mathematical function

Sin embargo, si deseamos que el resto del código de la función continúe ejecutándose independientemente del resultado de la operación que de error debemos utilizar try.

f2 <- function(x) {
  try(log(x))
  "Hola"
}

f2("test")
Error in log(x) : non-numeric argument to mathematical function
[1] "Hola"

Como vemos, se imprime en pantalla el mensaje de error producido por la funcion dentro de try. Si queremos que estos mensajes no aparezcan debemos indicar la opcion silent = TRUE como argumento de try.

f3 <- function(x) {
  try(log(x), silent = TRUE)
  "Hola"
}

f3("test")
[1] "Hola"

Si queremos pasar más de una operación dentro del try debemos encerrar estas entre llaves.

try({
  a <- 1
  b <- "test"
  a + b
})

También podemos capturar la salida de la función try. En tal caso lo que obtendremos será, en caso de que el bloque try finalice sin errores, el resultado de la ultima expresión evaluada. Si en cambio se presenta un error durante la operación, lo que obtendremos serán los mensajes de error contenidos en un objeto clase “try-error”.

success <- try(1 + 2)
success
[1] 3
class(success)
[1] "numeric"

failure <- try("a" + "b")
Error in "a" + "b" : non-numeric argument to binary operator
failure
[1] "Error in \"a\" + \"b\" : non-numeric argument to binary operator\n"
attr(,"class")
[1] "try-error"
attr(,"condition")
<simpleError in "a" + "b": non-numeric argument to binary operator>
class(failure)
[1] "try-error"

Un ejemplo útil de este tipo de la función try es cuando realizamos operaciones sobre una lista de objetos.

elementos <- list(1:5, -2: 3, letters[1:5])
elementos
[[1]]
[1] 1 2 3 4 5

[[2]]
[1] -2 -1 0 1 2 3

[[3]]
[1] "a" "b" "c" "d" "e"

res1 <- lapply(elementos, log)
Error in FUN(X[[i]], ...) : non-numeric argument to mathematical function
In addition: Warning message:
In FUN(X[[i]], ...) : NaNs produced
res1
Error: object 'res1' not found

res2 <- lapply(elementos, function(x) try(log(x)))
Error in log(x) : non-numeric argument to mathematical function
In addition: Warning message:
In log(x) : NaNs produced
res2
[[1]]
[1] 0.0000000 0.6931472 1.0986123 1.3862944 1.6094379

[[2]]
[1] NaN NaN -Inf 0.0000000 0.6931472 1.0986123

[[3]]
[1] "Error in log(x) : non-numeric argument to mathematical function\n"
attr(,"class")
[1] "try-error"
attr(,"condition")
<simpleError in log(x): non-numeric argument to mathematical function>

Dado que no hay una función que identifique la clase try-error automáticamente, podemos definir esta a mano.

is.error <- function(x) inherits(x, “try-error”)

is.error(res2[[1]])
[1] FALSE
is.error(res2[[2]])
[1] FALSE
is.error(res2[[3]])
[1] TRUE

 

 

tryCatch

Con try podíamos evitar que la aparición de un error detenga la ejecución del programa, pero no nos permite en forma simple tomar una acción en particular en consecuencia. Para lograr esto tenemos la función tryCatch, la cual de presentarse un error dentro de ella ejecuta un código para manejar tal error (handler). En realidad, puede manejar tantos errors como warnings o messages. Cuando se presenta uno de estos en el código a continuación se ejecuta la función asociada.

f <- function(code) {
  tryCatch(code,
  error = function(c) "test: error",
  warning = function(c) "test: warning",
  message = function(c) "test: message"
  )
}

f(stop("!"))
[1] "test: error"

f(warning("?!"))
[1] "test: warning"

f(message("?"))
[1] "test: message"

f(1)
[1] 1

tryCatch tiene un ultimo argumento, finally, el cual especifica un bloque de código que se ejecuta al finalizar el código principal independientemente del resultado final de este. Este se lo suele usar para tareas de limpieza como cerrar conexiones a bases de datos, eliminar variables que no se vuelvan a utilizar o eliminar archivos.

 

 

Comentarios

2 Comments

  1. Julián

    Hola Mauricio, ¡muy bueno el post, sigue así! Me suscribo, espero recibir nuevos artículos pronto.

    Reply
    • Mauricio

      Gracias Julian! Pronto estaré subiendo nuevos post. Saludos.

      Reply

Submit a Comment

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

This site uses Akismet to reduce spam. Learn how your comment data is processed.