Paulo Henrique Rodrigues Pinheiro

Um blog sobre programação para programadores!


Debugando em Python

Iniciando a usar o módulo pdb do Python para debugar com maestria, na linha de comando

Bug

Sou do time que aprendeu a usar print() para debugar. Isso não é um grande problema, mas tenho que admitir que usar uma ferramenta para debug é muito mais eficiente. Enfim, para um dado problema, use a ferramenta mais adequada.

Mas o uso de um debugger (esse é o nome) vai além da caça de erros. Serve para experimentação, testes e até mesmo aprendizagem de novas bibliotecas. Antes de tudo, o básico de como usar.

Em seu programa, você pode escolher pontos de parada, em que deseja explorar o que está em memória e chamar funções. Esses pontos de parada são isso mesmo que o nome diz: quando você executa seu programa, ao encontrar um ponto de parada, a execução é interrompida e está disponibilizado todo o estado atual de execução, além de um prompt do Python, para executar os comandos que bem desejar, e com o extra de certos comandos específicos a esse processo.

Aqui temos um pequeno, pequeno mesmo, programa com ponto de parada (note-se que pdb significa Python Debugger):


$ cat example1.py
breakpoint()
$ python example1.py
--Return--
/home/phrp/repo/debugger/example1.py(1)<module>()->None
-> breakpoint()
(Pdb) l
1 ->    breakpoint()
[EOF]
(Pdb) c

Nesse primeiro exemplo, temos uma simples instrução, que diz ao Python que chegando nesse ponto o programa deve suspender a execução, disponibilizando um prompt para que comandos sejam digitados e executados.

O primeiro comando ("l" de list), lista as onze linhas adjacentes à em que estamos.

O próximo comando (“c“, de continue, ou cont), segue o fluxo de execução. Como não se tem mais nenhum comando, encerra-se a execução.

Olhe as variáveis, siga em frente

Personagens da Carreta Furacão

“siga em frente, olhe para os lados” - péssimo conselho!

Além desses comandos, pode-se digitar o nome de variáveis para verificar seu valor (print não é necessário,e temos os comandos “p” e “pp“ (pretty print), criar novas variáveis, importar módulos, enfim, qualquer coisa que se faça num programa, contando com todo o contexto da execução naquele ponto.

Vamos inspecionar uma variável agora:


$ python example2.py
/home/phrp/repo/debugger/example2.py(2)<module>()
-> name = "Lambda "
(Pdb) l
1   breakpoint()
2 ->    name = "Lambda "
3   print(name * 3)
[EOF]
(Pdb) name
*** NameError: name 'name' is not defined
(Pdb) n
/home/phrp/repo/debugger/example2.py(3)<module>()
-> print(name * 3)
(Pdb) name
'Lambda '
(Pdb) c

Lambda Lambda Lambda

Aqui, usamos o comando "l" para listar o código mais próximo (onze linhas no máximo), depois instruímos o debugar a mostrar o valor em name. Como essa variável ainda não tinha sido definida, ocorreu uma exceção. Em seguida, instruímos que a próxima linha seja executada (2 -> name = "Lambda "), então novamente pedimos o valor da variável, agora definida, então indicamos a continuação do fluxo com o comando "c", que por fim imprimiu o valor da variável três vezes, como indicado no programa.

Aprofundando no código

Além do comando "n" (next) há o comando "s" (step) similar a ele, mas ao invés de apenas executar o próximo comando, se possível, ele entra na função chamada e para na primeira oportunidade. Uma forma de ir se aprofundando no código:


$ python example3.py
/home/phrp/repo/debugger/example3.py(2)<module>()
-> import this
(Pdb) l
1   breakpoint()
2 ->    import this
[EOF]
(Pdb) s
--Call--
<frozen importlib._bootstrap>(1167)_find_and_load()
(Pdb) r
The Zen of Python, by Tim Peters
Beautiful is better than ugly.
Explicit is better than implicit.
Simple is better than complex.
Complex is better than complicated.
Flat is better than nested.
Sparse is better than dense.
Readability counts.
Special cases aren't special enough to break the rules.
Although practicality beats purity.
Errors should never pass silently.
Unless explicitly silenced.
In the face of ambiguity, refuse the temptation to guess.
There should be one-- and preferably only one --obvious way to do it.
Although that way may not be obvious at first unless you're Dutch.
Now is better than never.
Although never is often better than right now.
If the implementation is hard to explain, it's a bad idea.
If the implementation is easy to explain, it may be a good idea.
Namespaces are one honking great idea -- let's do more of those!
--Return--
<frozen importlib._bootstrap>(1175)_find_and_load()-><module 'this...3.11/this.py'>
(Pdb) c

Aqui também, primeiro listamos o programa (comando “l”), e então instruímos o debugger a “entrar” no próxima linha e para o quanto antes (comando “s”, de step). Entramos com o comando “r” (return), que executa as linhas dentro da função corrente. Saindo da função que implementa o import this, é apresentado um resultado, e então finalizamos com o comando "c" (continue), pois não há mais comandos a executar. Para mais informações sobre o import this, veja esse texto.

Argumentos

Vamos a mais um comando, “a”, de args, que mostra quais argumentos foram passados para a função em que estivermos.


$ python example4.py
/home/phrp/repo/debugger/example4.py(7)<module>()
-> definition = make_dict('empresa', 'Musa')
(Pdb) l
2    new_dict = {x: y}
3    return new_dict
4   
5   
6   breakpoint()
7 ->    definition = make_dict('empresa', 'Musa')
8   print(definition)
[EOF]
(Pdb) s
--Call--
/home/phrp/repo/debugger/example4.py(1)make_dict()
-> def make_dict(x, y):
(Pdb) a
x = 'empresa'
y = 'Musa'
(Pdb) r
--Return--
/home/phrp/repo/debugger/example4.py(3)make_dict()->{'empresa': 'Musa'}
-> return new_dict
(Pdb) n
/home/phrp/repo/debugger/example4.py(8)<module>()
-> print(definition)
(Pdb) definition
{'empresa': 'Musa'}
(Pdb) c
{'empresa': 'Musa'}

Parando nas paradas

Algumas vezes queremos paradas rápidas, para inspeções surpresa :). Podemos definir vários pontos de parada, e apenas interromper o fluxo neles.


$ python example5.py
/home/phrp/repo/debugger/example5.py(3)one()
-> print('one')
(Pdb) l
1   def one():
2    breakpoint()
3 ->     print('one')
4   
5   
6   def two():
7    print('two')
8   
9   
10  def three():
11   breakpoint()
(Pdb) l 1,20
1   def one():
2    breakpoint()
3 ->     print('one')
4   
5   
6   def two():
7    print('two')
8   
9   
10  def three():
11   breakpoint()
12   print('three')
13  
14  
15  one()
16  two()
17  three()
[EOF]
(Pdb) c
one
two
/home/phrp/repo/debugger/example5.py(12)three()
-> print('three')
(Pdb) c
three

Nesse exemplo, primeiro listamos o código, mas como o comando lista apenas onze linhas de código, indicamos o que listar com o comando l 1,20, mostrando código entre as linhas indicadas. Ao parar na função one(), indicamos para o debugger ir até o próximo ponto de parada, que está na função three(), e com mais um comando "c", o programa encerra por não ter mais instruções.

Concluindo

Há mais opções e possibilidades no comado pdb do Python, que em sua base, são as mesmas de um debugger em C. Ter um conhecimento dessa ferramenta, e de técnicas básicas de debug, podem acelerar investigações, e torná-las algo não monótono também. E outro coisa que gostaria de reforçar, é que essa ferramenta se presta também a conhecer novas bibliotecas em um contexto mais rico de execução de um sistema existente, por exemplo. Defina um conveniente breakpoint() e então importe módulos, instancie classes e use os dados do contexto de sua aplicação.