Debugando um problema de recursão no Python
Em um fórum que participo apareceu uma dúvida e a forma como a interação fluiu para encontrarmos a solução foi bem interessante. Então compartilho um resumo neste blog, assim, talvez ajudamos mais gente ainda!
A ideia aqui é falar sobre perder o medo de ler as mensagens de erro e usá-las como guia para resolver problemas, para encontrar o que está dando problema e então, ser precisa em saber o que precisa ser solucionado. Primeiro, ajudei a pessoa a ler o a mensagem de erro e relacioná-la ao código que a pessoa havia escrito. Depois, entendemos a mensagem de erro e, por fim, propusemos uma solução.
Mas começemos pelo princípio! A mensagem incial dizia:
Estou tentando fazer uma simulação bem básica do mecanismo de prova de trabalho da blockchain:
E estou tendo um erro:
Encontrando o problema
A primeira coisa que notei é que a falta do traceback completo não ajudava a gente a ajudar a pessoa que tinha dúvida.
O traceback é o histórico do erro, da chamada do código que a gente escreveu, passando por toda estrutura da aplicação, biblioteca de terceiros ou código fonte do Python, até chegar na linha que gera o erro. Como diz o Henrique Bastos:
Quando um comando dá errado ou o resultado não é o esperado, é muito importante publicar a entrada e a saída na íntegra.
Printar a tela geralmente oculta informações importantes. Por isso é importante que você selecione no terminal, copie e cole as linhas na íntegra desde o comando até o final da mensagem de erro.
Pedi então ao amigo que estava com a dúvida, o traceback todo e a coisa ficou melhor:
Lendo esse trecho ficou claro o trajeto na execução do código, começando pela linha 37 — generate('t1, t2, t3, t4', '00'):
Essa chamada da função generate leva a execução ao ponto onde a função generate é definida, e a execução prossegue dali até a linha 14 — generate_with_nonce(data, dificult, 0), com uma chamada de outra função:
E assim por diante até a última linha do código executada que a pessoa havia escrito, a linha 10:
Em outras palavras, agora eu sabia precisamenete qual parte do código causava o erro:
O problema aí foi que essa linha fazia muita coisa:
- Tem uma conversão para string em str(nonce)
- Tem um concatenação de string com data +
- Tem uma conversão para bytes com o .encode(…)
- Tem a criação de um hash com o sha256(…)
- Tem um cálculo de hexadecimal com .hexdigest()
Alguns desses passos podem ter ainda conversões de objetos para string por debaixo dos panos. Então fica difícil saber o que exatamente está causando o erro. Eu sugeri tentar isolar esses passos para identificar qual passo dá problema, e aí sim pensar em uma solução mais precisa. Sugeri esse refactor para essa linha:
Bingo. Executando com essa nova versão e usando a mesma estratégia, descobrimos que o RecursionError estava exatamente em nonce = str(nonce). Excelente!
Entendendo o erro
Como a pessoa bem resumiu, o RecursionError é o que acontece no Python quando uma função recursivamente chama outra várias vezes – milhares de vezes, fazendo o computador achar que está em um loop infinito. Por segurança, então, ele pára esse ciclo com esse tipo de erro.
Analisando o código vi que a cada chamada da generate_with_nonce, o valor de data era passado para mod_hash — e generate_with_nonce é uma função recursiva, ou seja, ela se chama diversas vezes. No entanto reparei também que o valor de data não mudava a cada chamada recursiva, era sempre o mesmo que a primeira chamada, a que desencadeia a exexecução recursiva. Em outras palavras, poderíamos executar o nonce = str(nonce) apenas uma vez, na primeira execução da generate_with_nonce, e apenas se nonce ainda não fosse bytes.
Solução proposta
Com base nessa análise, sugeri o seguinte refactor:
Assim, problema resolvido, com um pouco de reflexão lendo o fluxo de execução, o famoso traceback : )
Discussion in the ATmosphere