Pular para o conteúdo principal

Debugging

Debugging é o processo de em caso de erros, comportamento inesperado ou em necessidade de análise detalhada de um algoritmo, efetuar a execução linha a linha inspecionando os objetos da stack.

A maior útilidade de debugging é descobrir a raiz de um problema não previsto em um algoritmo, Python oferece algumas abordagens para debugging.

Vamos pegar como exemplo nosso programa que repete vogais em um texto e criar uma função:

tembug.py

def repete_vogal(word):
"""Retorna a palavra com as vogais repetidas."""
final_word = ""
for letter in word:
if letter.lower() in "aeiouãõâôêéáíó":
final_word = letter * 2
else:
final_word = letter
return final_word
print(repete_vogal("banana"))

Ao executar esperamos a impressão de baanaanaa no terminal mas o que temos é aa o que significa que nosso programa tem um bug.

Execução interativa

python -i tembug.py
# aqui estamos em um terminal onde temos acesso a função
>>> repete_vogal("Bruno")
'oo'

Prints

Prints são uma das formas úteis e práticas de debugging, quando podemos executar o programa indefinidamente para reproduzir o erro e no meio da execução acompanhar a saída do terminal o uso de um print é com certeza a solução e junto com f-string torna-se uma ferramenta bastante completa.

def repete_vogal(word):
"""Retorna a palavra com as vogais repetidas."""
final_word = ""
for index, letter in enumerate(word):
# usamos enumerate para ajudar a sabermos as voltas do loop
print(f"{index=} {letter=}")
if letter.lower() in "aeiouãõâôêéáíó":
final_word = letter * 2
else:
final_word = letter
print(f"{final_word=}")
return final_word
print(repete_vogal("banana"))

Ao executar

index=0 letter='b'
final_word='b'
index=1 letter='a'
final_word='aa'
index=2 letter='n'
final_word='n'
index=3 letter='a'
final_word='aa'
index=4 letter='n'
final_word='n'
index=5 letter='a'
final_word='aa'
aa

Usando {variavel=} a partir do Python 3.8 em uma f-string teremos uma impressão fácil para debugging.

Com esta saída do print já podemos descobrir o que está acontecendo, porém se ainda assim não tenha ficado claro podemos partir para outras abordagens.

pdb

pdb é a ferramenta padrão de debugging interativo do Python e além de ser uma ferramenta bastante tradicional também se tornou um padrão adotado por todos os outros debuggers.

Executando o script com pdb

python -m pdb tembug.py                                
> /home/rochacbruno/Projects/python-base/tembug.py(1)<module>()
-> def repete_vogal(word):
(Pdb)

O debugger irá iniciar a execução do script porém irá parar na primeira linha e ficar esperando nosso comando, além disso irá expor um prompt onde podemos interagir com a stack de execução.

O pdb tem alguns comandos úteis que pode visualizar digitando ? ou help

(Pdb) help
Documented commands (type help <topic>):
========================================
EOF c d h list q rv undisplay
a cl debug help ll quit s unt
alias clear disable ignore longlist r source until
args commands display interact n restart step up
b condition down j next return tbreak w
break cont enable jump p retval u whatis
bt continue exit l pp run unalias where
Miscellaneous help topics:
==========================
exec pdb

Vamos abordar apenas alguns deles:

Digitando l ele mostra qual a linha de execução atual:

(Pdb) l
1 -> def repete_vogal(word):
2 """Retorna a palavra com as vogais repetidas."""

Digitando n ele executa o bloco atual e pula para o próximo:

(Pdb) n
> /home/rochacbruno/Projects/python-base/tembug.py(16)<module>()
-> print(repete_vogal("banana"))

Digitando s ele entra na execução de uma chamada de função repare no __call__:

(Pdb) s
--Call--
> /home/rochacbruno/Projects/python-base/tembug.py(1)repete_vogal()
-> def repete_vogal(word):

E então digitando n seguidas vezes vai passo a passo executando a função:

(Pdb) n
> /home/rochacbruno/Projects/python-base/tembug.py(3)repete_vogal()
-> final_word = ""
(Pdb) n
> /home/rochacbruno/Projects/python-base/tembug.py(4)repete_vogal()
-> for index, letter in enumerate(word):
(Pdb) n
> /home/rochacbruno/Projects/python-base/tembug.py(6)repete_vogal()
-> print(f"{index=} {letter=}")
(Pdb) n
index=0 letter='b'
> /home/rochacbruno/Projects/python-base/tembug.py(7)repete_vogal()
-> if letter.lower() in "aeiouãõâôêéáíó":
(Pdb) letter
'b'
(Pdb) index
0

E em cada instrução podemos visualizar o valor de variáveis definidas, e para saber onde estamos basta digitar l

(Pdb) l
2 """Retorna a palavra com as vogais repetidas."""
3 final_word = ""
4 for index, letter in enumerate(word):
5 # usamos enumerate para ajudar a sabermos as voltas do loop
6 print(f"{index=} {letter=}")
7 -> if letter.lower() in "aeiouãõâôêéáíó":
8 final_word = letter * 2
9 else:
10 final_word = letter
11
12 print(f"{final_word=}")

Para forçar o programa a rodar até uma determinada linha podemos criar breakpoints

(Pdb) b 12
Breakpoint 1 at /home/rochacbruno/Projects/python-base/tembug.py:12

E então pressionando c o programa irá executar até chegar ao breakpoint definido na linha 12.

(Pdb) c
> /home/rochacbruno/Projects/python-base/tembug.py(12)repete_vogal()
-> print(f"{final_word=}")

E pressionando c novamente o programa roda até a próxima volta do loop e assim consecutivamente.

(Pdb) c
final_word='b'
index=1 letter='a'
> /home/rochacbruno/Projects/python-base/tembug.py(12)repete_vogal()
-> print(f"{final_word=}")
(Pdb) c
final_word='aa'
index=2 letter='n'
> /home/rochacbruno/Projects/python-base/tembug.py(12)repete_vogal()
-> print(f"{final_word=}")
(Pdb) c
final_word='n'
index=3 letter='a'
> /home/rochacbruno/Projects/python-base/tembug.py(12)repete_vogal()
-> print(f"{final_word=}")
(Pdb) c
final_word='aa'
index=4 letter='n'
> /home/rochacbruno/Projects/python-base/tembug.py(12)repete_vogal()
-> print(f"{final_word=}")

Digitando l irá mostrar onde está o breakpoint

(Pdb) l
7 if letter.lower() in "aeiouãõâôêéáíó":
8 final_word = letter * 2
9 else:
10 final_word = letter
11
12 B-> print(f"{final_word=}")
13 return final_word
14
15
16 print(repete_vogal("banana"))

e digitando b mostra os breakpoints definidos e quantas vezes já foram executados.

(Pdb) b
Num Type Disp Enb Where
1 breakpoint keep no at /home/rochacbruno/Projects/python-base/tembug.py:12
breakpoint already hit 6 times

E para remover um breakpoint basta chamar o comando disable seguido do Num

(Pdb) disable 1
Disabled breakpoint 1 at /home/rochacbruno/Projects/python-base/tembug.py:12

e por fim digitando c o programa continua executando até o final.

(Pdb) c
final_word='aa'
aa
The program finished and will be restarted

Para sair do debugger utilize q ou ctrl + C

Usando breakpoints condicionais

Para executar até uma determinada linha:

$ python -m pdb -c "until 12" tembug.py 
> /home/rochacbruno/Projects/python-base/tembug.py(16)<module>()
-> print(repete_vogal("banana"))
(Pdb)

Para executar somente em caso de uma exception ser carregada:

altere a linha 3 do arquivo para forçar uma exception:

def repete_vogal(word):
"""Retorna a palavra com as vogais repetidas."""
1 / 0
final_word = ""

e execute com -c continue

$ python -m pdb -c continue tembug.py
Traceback (most recent call last):
File "/usr/lib/python3.10/pdb.py", line 1723, in main
pdb._runscript(mainpyfile)
File "/usr/lib/python3.10/pdb.py", line 1583, in _runscript
self.run(statement)
File "/usr/lib/python3.10/bdb.py", line 597, in run
exec(cmd, globals, locals)
File "<string>", line 1, in <module>
File "/home/rochacbruno/Projects/python-base/tembug.py", line 17, in <module>
print(repete_vogal("banana"))
File "/home/rochacbruno/Projects/python-base/tembug.py", line 3, in repete_vogal
1 / 0
ZeroDivisionError: division by zero
Uncaught exception. Entering post mortem debugging
Running 'cont' or 'step' will restart the program
> /home/rochacbruno/Projects/python-base/tembug.py(3)repete_vogal()
-> 1 / 0
(Pdb) word
'banana'

Breakpoints explicitos

Em qualquer linha de código onde deseja que o debugger pare a execução:

import pdb;pdb.set_trace()

ou

__import__("pdb").set_trace()

ou a partir do Python 3.7

breakpoint()

Variações do protocolo pdb

ipdb

pip install ipdb

Ao trocar pdb por ipdb e executar teremos a mesma experiência porém dentro de um terminal ipython

$ python -m ipdb -c "until 16" tembug.py
> /home/rochacbruno/Projects/python-base/tembug.py(16)<module>()
14
15
---> 16 print(repete_vogal("banana"))
ipdb>

Com a vantagem de um comando adicional chamado interact que permite redefinir objetos e funções com facilidade.

ipdb> interact
*interactive*
In [1]: repete_vogal("banana")
Out [2]: 'aa'
In [2]: exit
*exiting interactive console...*
ipdb>

É possível configurar o ipdb como debugger default para o Python através de uma variável de ambiente:

export PYTHONBREAKPOINT=ipdb.set_trace                 
❯ python tembug.py
> /home/rochacbruno/Projects/python-base/tembug.py(6)repete_vogal()
5 breakpoint()
----> 6 if letter.lower() in "aeiouãõâôêéáíó":
7 final_word = letter * 2
ipdb>

Debugger visual no terminal

pip install pudb

E substituindo por pudb

python -m pudb tembug.py

Debugging pudb

O VSCode também tem um debugger integrado que permite a interação visual e através do terminal.

Debugging pudb

E afinal onde estava o bug?

O erro está dentro do loop for.

Código errado:

def repete_vogal(word):
"""Retorna a palavra com as vogais repetidas."""
final_word = ""
for letter in word:
if letter.lower() in "aeiouãõâôêéáíó":
final_word = letter * 2 # <- Aqui tem que ser `+=`
else:
final_word = letter # <- Aqui tem que ser `+=`
return final_word
print(repete_vogal("banana"))

Código correto

def repete_vogal(word):
"""Retorna a palavra com as vogais repetidas."""
final_word = ""
for letter in word:
if letter.lower() in "aeiouãõâôêéáíó":
final_word += letter * 2
else:
final_word += letter
return final_word
print(repete_vogal("banana"))