pdb, le débogueur de python

extrait de http://cdadhemar.free.fr/gtux/post/2009/06/18/Pdb%2C-le-d%C3%A9bogueur-Python-int%C3%A9gr%C3%A9. Quand il s’agit de déboguer du code python la plus part des gens utilisent un print “debug” bien placé pour savoir si on passe à un endroit ou un print “ma_variable = ‘%s’” % ma_variable pour connaitre la valeur d’une variable dans le flux d’exécution. Pourtant python dispose d’un débogueur intégré très puissant: pdb. Que ceux qui sont allergiques à gdb, le débogueur C/C++, soient rassurés: bien qui pdb ressemble à bien des égards à gdb c’est un débogueur python pour python. Il est à la fois simple et très puissant. Nous allons voir deux cas simples d’utilisation qui seront utiles, je l’espère, aux développeurs expérimentés comme aux développeurs occasionnels. Pour l’exemple nous utiliserons le script suivant :

#!/usr/bin/env python
def func1(i):
    if i == 0:
        raise Exception("Exception in func1")

def func2(i):
    func1(i)

def func3(i):
    func2(i)

if __name__ == '__main__':
    func3(0)

La traceback surprise Vous développez ou utilisez votre logiciel préféré et BOUM une traceback :

Traceback (most recent call last):
  File "test.py", line 14, in <module>
    func3(0)
  File "test.py", line 11, in func3
    func2(i)
  File "test.py", line 8, in func2
    func1(i)
  File "test.py", line 5, in func1
    raise Exception("Exception in func1")
Exception: Exception in func1

Vous n’avez pas accès au code en écriture ou le cas est trop complexe pour être géré avec des print bien placés ; pdb va vous aider. Depuis python 2.5 vous pouvez exécuter des modules python comme des scripts. On va donc lancer le script en utilisant pdb :

python -m pdb test.py

On se retrouve dans pdb :

[chicha@grogro Bureau]$ python -m pdb test.py
> /home/chicha/Bureau/test.py(3)<module>()
-> def func1(i):

A ce stade le programme n’a toujours pas été lancé. On le lance via la commande pdb continu (c). Le programme entre en mode “post mortem” dès qu’il se prend une exception non catchée :

Traceback (most recent call last):
  File "/usr/lib64/python2.6/pdb.py", line 1276, in main
    pdb._runscript(mainpyfile)
  File "/usr/lib64/python2.6/pdb.py", line 1193, in _runscript
    self.run(statement)
  File "/usr/lib64/python2.6/bdb.py", line 366, in run
    exec cmd in globals, locals
  File "<string>", line 1, in <module>
  File "test.py", line 14, in <module>
    func3(0)
  File "test.py", line 11, in func3
    func2(i)
  File "test.py", line 8, in func2
    func1(i)
  File "test.py", line 5, in func1
    raise Exception("Exception in func1")
Exception: Exception in func1
Uncaught exception. Entering post mortem debugging
Running 'cont' or 'step' will restart the program
> /home/chicha/Bureau/test.py(5)func1()
-> raise Exception("Exception in func1")

Voilà la séance de debugging peut commencer ... Bye bye print

Pour commencer on aimerait bien savoir où l’on est dans la pile de fonction et à quoi ressemble le code autour de là où on est. C’est le role des commandes backtrace (bt) et list (l) :

(Pdb) bt
  /usr/lib64/python2.6/bdb.py(371)run()
-> sys.settrace(None)
  <string>(1)<module>()
  /home/chicha/Bureau/test.py(3)<module>()
-> def func1(i):
  /home/chicha/Bureau/test.py(11)func3()
-> func2(i)
  /home/chicha/Bureau/test.py(8)func2()
-> func1(i)
> /home/chicha/Bureau/test.py(5)func1()
-> raise Exception("Exception in func1")
(Pdb) l
  1         #!/usr/bin/env python
  2
  3         def func1(i):
  4             if i == 0:
  5  ->                 raise Exception("Exception in func1")
  6
  7         def func2(i):
  8             func1(i)
  9
 10         def func3(i):
 11             func2(i)
(Pdb)

Ensuite on aimerait bien connaitre la valeur des arguments passés en paramètres, c’est le rôle de la commande args :

(Pdb) args
i = 0
(Pdb)

Enfin on peut afficher la valeur de n’importe quelle variable ou expression python valide à l’endroit où l’on se trouve dans le code, en utilisant la commande print (p) :

(Pdb) p i
0
(Pdb) p 2 * 2
4
(Pdb)

pdb fournit évidemment bien plus de commandes que cela, vous pouvez par exemple : * Monter et descendre où vous voulez dans la pile d’appels de fonctions, * Mettre des breakpoint, conditionnels ou non, temporaires ou non, * Associer des commandes à des breakpoints, * Exécuter du code ligne par ligne, fonctions par fonctions, * etc ... Vous avez tout ce qu’il faut dans la documentation de pdb Si vous avez une question n’hésitez pas à me contacter ! Mais avant de terminer cet article je voudrai vous montrer une autre façon d’utiliser pdb :

pdb ou le debugging chirurgical

Vous avez accès au code en écriture, Vous exécutez un programme très long et vous n’avez pas envie de débugger tout, mais seulement le bout de code qui vous intéresse ? Alors pdb.set_trace() est fait pour vous ! Je reprend le code précédent, j’importe pdb et je place un petit pdb.set_trace() là ou je veux commencer à débugger :

#!/usr/bin/env python
import pdb

def func1(i):
    pdb.set_trace()
    if i == 0:
        raise Exception("Exception in func1")

def func2(i):
    func1(i)

def func3(i):
    func2(i)

if __name__ == '__main__':
    func3(0)

Je lance mon pogramme normalement (sans utiliser -m pdb) et pouf ! Le programme s’exécute jusqu’à la ligne pdb.set_trace(). Ensuite pdb se lance et vous avez le debuggeur à votre disposition !

Bien sûre avec mon exemple simpliste on voit mal l’intérêt de la chose. Mais dès que le code se complique avec des boucles for sur des milliers d’éléments avec un bug au 700 ième on est bien content de pouvoir faire un truc du genre :

for i in range(1000):
    if i = 700:
        pdb.set_trace()
        ...

J’espère que ça permettra aux plus réticents, que gdb a effrayé, de se lancer ! exemple d’utilisation concrête

#!/usr/bin/env python
import pdb
def func1(i):
    if i == 0:
        raise Exception("Exception in func1")

def func2(i):
    func1(i)

def func3(i):
    j = 2
    func2(i)

if __name__ == '__main__':
    pdb.set_trace()
    func3(0)

Il faut noter que le débugger ce lancera avant l’appel de func3

lancement du programme

C:\Users\aoustin\Desktop>python test.py
> c:\users\aoustin\desktop\test.py(16)<module>()
-> func3(0)
(Pdb) s
--Call--
> c:\users\aoustin\desktop\test.py(11)func3()
-> def func3(i):
(Pdb) args
i = 0
(Pdb) s
> c:\users\aoustin\desktop\test.py(11)func3()
-> j = 2
(Pdb) print j
2
(Pdb) s
> c:\users\aoustin\desktop\test.py(12)func3()
-> func2(i)
(Pdb) n
Exception: Exceptio... func1',)
> c:\users\aoustin\desktop\test.py(12)func3()
-> func2(i)
(Pdb) s
--Return--
> c:\users\aoustin\desktop\test.py(12)func3()->None
-> func2(i)
(Pdb) s
Exception: Exceptio... func1',)
> c:\users\aoustin\desktop\test.py(16)<module>()
-> func3(0)
(Pdb) n
--Return--
> c:\users\aoustin\desktop\test.py(16)<module>()->None
-> func3(0)
(Pdb) n
Traceback (most recent call last):
  File "test.py", line 16, in <module>
    func3(0)
  File "test.py", line 12, in func3
    func2(i)
  File "test.py", line 9, in func2
    func1(i)
  File "test.py", line 6, in func1
    raise Exception("Exception in func1")
Exception: Exception in func1

Il faut noter l’utilisation des commandes: * s: permet d’avancer d’une ligne * n: permet d’avancer d’une fonction * args: visualisation des arguments * print : utilisation de la fonction print pour visualiser une valeur

les commandes

h(elp) [commande] Sans argument, affiche la liste des commandes disponibles. Avec une commande comme argument, affiche l’aide concernant cette commande. “help pdb” affiche en entier le fihcier de documentation; si la variable d’environnement $PAGER est définie, le fichier est passé à la commande qui s’y trouve. Il faut faire “help exec” pour avoir l’aide de la commande “!” (seuls les identificateurs peuvent être des arguments).

w(here) Affiche une trace de pile, avec l’instance d’activation la plus récente en bas. Une flèche indique l’instance d’activation actuelle, qui détermine le contexte de la plupart des commandes.

d(own) Change l’instance d’activation actuelle pour celle qui est un niveau plus bas dans la pile (et qui est plus récente).

u(p) Change l’instance d’activation actuelle pour celle qui est un niveau plus haut dans la pile (et qui est plus ancienne).

b(reak) [[fichier:]nligne|fonction[, condition]] Avec l’argument nligne, pose un point d’arrêt à la ligne nligne du fichier actuel. Avec un argument fonction, pose un point d’arrêt à la premiére instruction exécutable de cette fonction. Le numéro de ligne peut être préfixé avec un nom de fichier et un deux-points, pour spécifier un point d’arrêt dans un autre fichier (probablement un fichier qui n’a pas encore été chargé). Le fichier est cherché dans sys.path. Notez que chaque point d’arrêt portera un numéro, auquel font référence toutes les commandes qui se servent des points d’arrêt. Si un second argument est donné, ce doit être une expression qui doit fournir une valeur vraie avant que le point d’arrêt ne soit respecté. Sans argument, affiche tous les points d’arrêt, avec pour chacun le nombre de fois qu’il a été atteint, son décompte à ignorer (voir ignore ci-dessous), et la condition associée lorsqu’elle existe.

tbreak [[fichier:]nligne|fonction[, condition]] Point d’arrêt temporaire, qui sera enlevé dès qu’il sera atteint pour la première fois. Les arguments sont les mêmes que pour break.

cl(ear) [numero_ptda [numero_ptda ...]] Avec argument, enlève les points d’arrêt dont les numéros sont donnés séparés par des espaces. Sans argument, enlève tous les points d’arrêt (mais demande confirmation avant).

disable [numero_ptda [numero_ptda ...]] Désactive la liste de points d’arrêt (séparés par des espaces) donnée en argument. Désactiver un point d’arrêt, contrairement a clear, ne l’efface pas de la liste des points d’arrêt existants; il pourra être réactivé par la suite.

enable [numero_ptda [numero_ptda ...]] (Ré)active les points d’arrêt spécifiés.

ignore numero_ptda [count] Change le décompte à ignorer du point d’arrêt spécifé. Si count n’est pas donné, le décompte à ignorer sera 0. Un point d’arrêt devient actif lorsque le décompte à ignorer devient nul. S’il ne l’est pas, il sera décrémenté à chaque passage par le point d’arrêt, si celui-ci n’est pas désactivé et sa condition est vraie.

condition numero_ptda [condition] Condition est une expression qui doit valoir vrai pour que le point d’arrêt soit respecté. Si la condition n’est pas donnée, la condition précédente est enlevée: le point d’arrêt sera rendu inconditionnel.

s(tep) Exécute la ligne actuelle, en s’arrêtant dès que possible (soit dans une fonction qui est appelée, soit à la prochaine ligne de la fonction courante).

n(ext) Poursuit l’exécution jusqu’à ce que la ligne suivante de la fonction soit atteinte, ou jusqu’à ce que la fonction retourne. (La différence entre “next” et “step” est que “step” s’arrête dans une fonction appelée, alors que “next” exécute un appel de fonction à vitesse (presque) normale, et s’arrêtera à la ligne suivante de la fonction actuelle.)

r(eturn) Continue l’exécution jusqu’à ce que la fonction actuelle retourne.

c(ont(inue)) Continue l’exécution et arrête uniquement sur un point d’arrêt.

l(ist) [debut[, fin]] Affiche le code source du fichier actuel. Sans arguments, affiche 11 lignes autour de la ligne actuelle, ou continue l’affichage du list précédent. Avec un seul argument, affiche 11 lignes autour de celle-là. Avec deux arguments, affiche le source entre les numéros de ligne donnés; si le second argument est négatif, il est interprété comme un décompte de lignes (à partir de debut).

a(rgs) Affiche la liste des arguments de la fonction actuelle.

p expression Evalue l’expression dans le contexte actuel et affiche son résultat. (Note: “print” peut aussi être utilisé, mais ce n’est pas une commande du débogueur — c’est l’instruction print de Python.)

alias [nom [commande]] Crée un alias appelé nom qui exécute la commande. La commande ne doit pas être donnée entre guillemets. Les paramètres qui peuvent être remplacés sont indiqués par “%1”, “%2”, etc., “%*” est remplacé par tous les paramètres. Si aucune commande est donnée, tous les alias sont affichés. Les alias peuvent être imbriqués, et contiennent tout ce qui peut être saisi à l’invite de pdb. Notez que les commandes interned de pdb peuvent être redéfinies par des alias. Une commande ainsi surchargée est cachée jusqu’à ce que son alias soit effacé. L’aliasage est apliqué de façon récursive au premier mot de la ligne de commande; tous les autres mots sont conservés.

unalias nom Efface un alias.

[!]instruction Exécute l’instruction (d’une seule ligne) dans le contexte de l’instance d’activation courante. Le point d’exclamation peut être omis lorsque le premier mot ne ressemble pas à une commande du débogueur. Pour changer la valeur d’une variable globale, il faut préfixer l’affectation avec une instruction “global” dans la même ligne, par exemple:

q(uit) Sort du débogueur. Le programme qui était exécuté est avorté.