Usando el depurador (pdb)

Tener un conocimiento general del depurador que Python incluye por defecto resulta una gran ventaja al momento de inspeccionar un script en busca del origen de un error. A menudo es útil incluir unas cuantas llamadas a las funciones print() o input() para detener el programa en una posición determinada y conocer el valor de algunos objetos, pero cuando nuestra base de código se vuelve lo suficientemente grande o simplemente esta técnica no es suficiente, el depurador juega un rol fundamental.

Antes de pasar a la explicación, veamos a grandes rasgos la definición de depurador según Wikipedia:

Un depurador (en inglés, debugger), es un programa usado para probar y depurar (eliminar) los errores de otros programas (el programa «objetivo»).

Ahora bien, llevado al mundo de Python, el depurador no es un programa sino un módulo que se incluye en la librería estándar: pdb. Existen otros depuradores para Python como Winpdb o aquellos incluidos en algunos entornos de desarrollo (Visual Studio, PyCharm) pero no serán tratados en este artículo. Por otro lado, el programa «objetivo» es un archivo de código de fuente de Python que queramos depurar. También podemos usar el depurador en la consola interactiva, como veremos más adelante.

pdb provee dos formas para llevar a cabo una depuración. La primera es la capacidad de instalar puntos de interrupción (breakpoints) para detener un script en una determinada posición. La segunda se denomina depuración post-mortem que, como su nombre lo indica, permite examinar el código luego de haberse producido el error. Esta última característica es generalmente útil en la consola interactiva.

Instalando puntos de interrupción

Este tipo de depuración, a través de breakpoints, es la que estarás usando en la mayoría de los casos en busca de la causa de un error en tu programa. Aquellos escenarios en donde se obtiene una excepción y conocemos la ubicación dentro del código de fuente en la que ésta se produce pero ignoramos su causa.

Consideremos el siguiente código de ejemplo, un simple script que contiene una función para dividir.

#!/usr/bin/env python
# -*- coding: utf-8 -*-

def div(a, b):
    """Return a / b."""
    return a / b

print(div(5, 0))

En este caso resulta trivial usar el depurador, pues rápidamente podemos observar que la función lanzará una excepción al intentar dividir por cero. Considérese a modo ilustrativo.

Traceback (most recent call last):
  File "test.py", line 8, in
    print(div(5, 0))
  File "test.py", line 6, in div
    return a / b
ZeroDivisionError: division by zero

Para depurar nuestra función, importamos el módulo pdb y luego llamamos a la función set_trace() para incluir un punto de interrupción. El programa se detendrá exactamente en el lugar en que hemos hecho la llamada.

import pdb

def div(a, b):
    """Return a / b."""
    pdb.set_trace()
    return a / b

print(div(5, 0))

Ejecutamos el script y obtenemos lo siguiente:

> /test.py(9)div()
-> return a / b
(Pdb)

La primera línea indica el archivo que estamos depurando, junto a la posición en donde se ha detenido el programa (el número de línea y la función correspondiente). La segunda muestra la próxima porción de código a ejecutar. Por ende la excepción todavía no ha ocurrido y procedemos a determinar su causa. En tercer lugar el depurador abre una consola interactiva y espera que le indiquemos algún comando.

Principales comandos

Puedes tipear h o help para ver una lista con los comandos disponibles. También help comando para obtener información sobre un comando específico.

Para interactuar con los objetos disponibles hasta la interrupción (por ejemplo, los valores de a y b) usamos el comando p seguido de una expresión (como el nombre de un objeto).

(Pdb) p a
5
(Pdb) p b
0
(Pdb) p a*5 + 10
35
(Pdb) p (b + 10) / a
2.0

Este comando acepta únicamente expresiones. Para crear nuevos objetos, usamos un signo de exclamación (!).

(Pdb) !c = 1
(Pdb) p a + b + c
6

Nótese que no hay espacio entre el signo de exlamación y la declaración.

A partir de la versión 3.2, el depurador incluye el comando interact que abre una consola interactiva de Python (similar a la que solemos utilizar) pero que incorpora los objetos disponibles al momento de la interrupción.

(Pdb) interact
*interactive*
>>> a
5
>>> b
0
>>> c
1
>>> def f(*args):
...     return sum(args)
...
>>> f(a, b, c)
6
>>> import os
>>> os.name
'nt'

Presiona CTRL + C (en Windows) o CTRL + D (Linux) para salir del modo interactivo y regresar al depurador.

Es importante aclarar que los cambios realizados a los objetos durante una sesión interactiva no son aplicados al código en depuración. Por el contrario, asignaciones con el comando ! sí modifican el comportamiento del programa.

Por ejemplo, podemos alterar el valor de b para evitar la división por cero.

(Pdb) !b = 1

Y luego usamos el comando continue para resumir la ejecución del script.

(Pdb) continue
5.0

Paseando por el código

El depurador permite ir avanzando en el código desde una posición determinada (donde se ubicó el breakpoint), ya sea línea por línea o «saltando» a otras partes del código. Así, podemos seguir cuidadosamente el flujo del programa, chequeando el valor de los objetos con los comandos vistos anteriormente, hasta encontrar la fuente del error.

Tomemos el siguiente código extraído de la web de Python, una función que imprime la sucesión de Fibonacci.

def fib(n):
    a, b = 0, 1
    while a < n:
        print(a, end=' ')
        a, b = b, a+b
    print()

fib(1000)

Nuevamente vamos a ubicar un punto de interrupción al comienzo de la función, en este caso luego de definir los objetos a y b.

import pdb

def fib(n):
    a, b = 0, 1
    pdb.set_trace()
    while a < n:
        print(a, end=' ')
        a, b = b, a+b
    print()

fib(1000)

El programa se detiene justo antes de ingresar al bucle. Podemos prever que este último se ejecutará ya que la condición se cumple:

> /test.py(10)fib()
-> while a < n:
(Pdb) p (a, n)
(0, 1000)
(Pdb) p a < n
True

pdb provee dos comandos para avanzar a la siguiente línea de código (ejecutar la acutal e interrumpir en la siguiente): n o next y s o step. La diferencia es que next considera únicamente el código dentro de un mismo ámbito. En otras palabras, step «ingresa» a las funciones y step las pasa por alto (en el depurador).

Por ejemplo, en el código anterior, una vez interrumpido el programa utilizamos next o step indistintamente pues while a < n: no es una llamada a una función.

(Pdb) next
> /test.py(10)fib()
-> print(a, end=' ')

Ahora bien, en esta línea usar un comando u otro no es indistinto. Estamos invocando una función, entonces, next avanzaría hacia la siguiente línea dentro del ámbito (a, b = b, a+b) mientras que step pasa a la primera línea dentro de print().

> /test.py(10)fib()
-> print(a, end=' ')
(Pdb) step
--Call--
> /cp850.py(18)encode()
-> def encode(self, input, final=False):

Los comandos next y step son conocidos en otros depuradores como Step Over y Step Into.

Podemos también ejecutar hasta una determinada línea o realizar saltos en el código, con algunas limitaciones.

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import pdb

def fib(n):
    a, b = 0, 1
    pdb.set_trace()
    while a < n:
        print(a, end=' ')
        a, b = b, a+b
    print()

fib(1000)

(Muestro el código completo ya que en este caso es necesario que el del lector coincida con el anterior, ya que nos estaremos refiriendo a los números de línea).

Con el comando until podemos indicarle al depurador que corra el programa hasta la línea especificada. Por ejemplo, until 12 ejecuta el código hasta el final de la función.

> /test.py(9)fib()
-> while a < n:
(Pdb) until 12
0 1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987 > /test.py(12)fib()
-> print()

Esto es similar a usar el comando return, que avanza hasta el final de una función (en caso de haber interrumpido dentro de una).

Para el siguiente ejemplo movamos el punto de interrupción a la primera línea dentro del bucle.

def fib(n):
    a, b = 0, 1
    while a < n:
        pdb.set_trace()
        print(a, end=' ')
        a, b = b, a+b
    print()

Corremos el programa y se detiene en la línea número 10. Si queremos evitar que esta línea sea ejecutada en esta ocasión, podemos emplear el comando jump para saltar a la línea subsiguiente (la número 11).

> /test.py(10)fib()
-> print(a, end=' ')
(Pdb) jump 11
> /test.py(11)fib()
-> a, b = b, a+b

Nótese que print(a, end=' ') no ha sido ejecutado. También podemos especificar saltos a posiciones anteriores. Por ejemplo, podemos vovler a la línea 10 y avanzar en la ejecución:

> /test.py(10)fib()
-> print(a, end=' ')
(Pdb) next
0 > /test.py(11)fib()
-> a, b = b, a+b

Incluso volver al inicio o saltar al final de la función vía jump 7 o jump 12, respectivamente.

Interrupciones condicionales

Podemos habilitar, deshabilitar y establecer condiciones en determinados puntos de interrupción. Esta característica funciona únicamente cuando los breakpoints son añadidos vía el comando break.

import pdb

def fib(n):
    a, b = 0, 1
    while a < n:
        print(a, end=' ')
        a, b = b, a+b
    print()

pdb.set_trace()
fib(1000)

Interrumpimos el comienzo del programa para poder emplear el comando. break opera con el nombre de una función o bien con un número de línea. Por ejemplo, para interrumpir la ejecución en la primera línea de la función fib():

(Pdb) break fib
Breakpoint 1 at /test.py:6

O bien:

(Pdb) break 7
Breakpoint 1 at /test.py:7

Comprobamos la interrupción:

(Pdb) continue
> /test.py(7)fib()
-> a, b = 0, 1

break también puede establecer interrupciones en otros archivos con la sintaxis break archivo.py:línea.

Al omitir los argumentos en el comando podemos ver una lista de los puntos de interrupción instalados.

(Pdb) break
Num Type         Disp Enb   Where
1   breakpoint   keep yes   at /test.py:6

Tomando como referencia el número del breakpoint, 1 en este caso, podemos deshabilitarlo con el comando disable 1 y re-habilitarlo vía enable 1.

Usando el comando condition seguido del número del breakpoint y una expresión, lo convertimos en condicional. La ejecución se detiene en su posición únicamente si la expresión es verdadera. Lo interesante es que podemos incluir los objetos disponibles, por ejemplo, el siguiente punto de interrupción es ejecutado únicamente cuando el archivo es llamado como un script (no así cuando es importado):

> /test.py(14)<module>()
-> fib(1000)
(Pdb) break fib
Breakpoint 1 at /test.py:6
(Pdb) break
Num Type         Disp Enb   Where
1   breakpoint   keep yes   at /test.py:6
(Pdb) condition 1 __name__ == "__main__"
New condition set for breakpoint 1.

Para remover la condición, simplemente usamos condition 1.

Podemos eliminar todos los puntos de interrupción usando clear o bien uno específico vía clear n, donde n corresponde al número del breakpoint.

Otros comandos
  • restart: reiniciar el programa manteniendo los puntos de interrupción.
  • q o quit: terminar el programa.

Puedes conocer los comandos remanentes no mencionados en este artíuclo en la documentación oficial de pdb.

Depuración post-mortem

Esta funcionalidad es generalmente útil en dos situaciones. La primera ocurre cuando tu programa finaliza con algún error pero no tienes idea de cómo o dónde ocurre. Puedes indicarle a Python que corra tu script a través del depurador.

python -m pdb script.py

El programa se detendrá antes de que pueda ejecutar cualquier operación, para que puedas establecer puntos de interrupción. Hecho esto, escribe continue para correr el código. Cuando ocurra una excepción que obligue al script a terminar, el depurador interrumpirá la ejecución en tal posición para que puedas usar las herramientas que provee.

La otra situación es al usar la consola interactiva. Si llamas a una función y ésta falla con una excepción, puedes iniciar el depurador luego de que el error halla ocurrido a través de la función pdb.pm() que actúa sobre la última excepción lanzada.

>>> import pdb
>>> def div(a, b):
...     return a / b
...
>>> </em>div(5, 0)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 2, in div
ZeroDivisionError: integer division or modulo by zero
>>> pdb.pm()
> <stdin>(2)div()
(Pdb) p a
5
(Pdb) p b
0

Nótese que en la depuración post-mortem, dado que el error ya ha ocurrido y la ejecución ha finalizado, no es posible usar comandos de control del flujo del programa como jump, until, continue, etc.

La función pdb.pm() opera en base a la última excepción lanzada. Si queremos habilitar la depuración post-mortem en el manejo de una excepción, en su lugar usamos pdb.post_mortem().

try:
    raise RuntimeError
except RuntimeError:
    pdb.post_mortem()

Curso online 👨‍💻

¡Ya lanzamos el curso oficial de Recursos Python en Udemy! Un curso moderno para aprender Python desde cero con programación orientada a objetos, SQL y tkinter en 2024.

Consultoría 💡

Ofrecemos servicios profesionales de desarrollo y capacitación en Python a personas y empresas. Consultanos por tu proyecto.

Deja una respuesta