Debugando um problema de recursão no Python

Cuducos July 6, 2020
Source

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:

  1. Tem uma conversão para string em str(nonce)
  2. Tem um concatenação de string com data +
  3. Tem uma conversão para bytes com o .encode(…)
  4. Tem a criação de um hash com o sha256(…)
  5. 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

Loading comments...