Tratamento profissional de erros com Python

Neste tutorial, você aprenderá a lidar com condições de erro em Python do ponto de vista de todo o sistema. O tratamento de erros é um aspecto crítico do design e vai desde os níveis mais baixos (às vezes o hardware) até os usuários finais. Se você não tiver uma estratégia consistente, seu sistema não será confiável, a experiência do usuário será ruim e você terá muitos desafios de depuração e solução de problemas.

A chave do sucesso é estar ciente de todos esses aspectos interligados, considerá-los explicitamente e formar uma solução que aborde cada ponto.

Códigos de status vs. exceções

Existem dois modelos principais de tratamento de erros: códigos de status e exceções. Os códigos de status podem ser usados ​​por qualquer linguagem de programação. Exceções requerem suporte a idioma/tempo de execução.

Python suporta exceções. Python e sua biblioteca padrão usam exceções liberalmente para relatar muitas situações excepcionais, como erros de IO, divisão por zero, indexação fora dos limites e também algumas situações não tão excepcionais, como final de iteração (embora esteja oculta). A maioria das bibliotecas segue o exemplo e levanta exceções.

Isso significa que seu código terá que lidar com as exceções levantadas pelo Python e pelas bibliotecas de qualquer maneira, então você também pode gerar exceções do seu código quando necessário e não depender de códigos de status.

Exemplo rápido

Antes de mergulhar no santuário interno das exceções do Python e das práticas recomendadas de tratamento de erros, vamos ver alguns tratamentos de exceção em ação:

Aqui está a saída ao chamar h():

Exceções do Python

As exceções do Python são objetos organizados em uma hierarquia de classes.

Aqui está toda a hierarquia:

Existem várias exceções especiais derivadas diretamente de BaseExceptionCurti SystemExit, KeyboardInterrupt e GeneratorExit. Então há o Exception class, que é a classe base para StopIteration, StandardError e Warning. Todos os erros padrão são derivados de StandardError.

Quando você gera uma exceção ou alguma função que você chamou gera uma exceção, esse fluxo de código normal termina e a exceção começa a se propagar na pilha de chamadas até encontrar um manipulador de exceção adequado. Se nenhum manipulador de exceção estiver disponível para tratá-lo, o processo (ou mais precisamente o thread atual) será encerrado com uma mensagem de exceção não tratada.

Criando exceções

Criar exceções é muito fácil. Você só usa o raise palavra-chave para levantar um objeto que é uma subclasse do Exception classe. Pode ser um exemplo de Exception em si, uma das exceções padrão (por exemplo, RuntimeError), ou uma subclasse de Exception você se derivou. Aqui está um pequeno trecho que demonstra todos os casos:

Capturando exceções

Você pega exceções com o except cláusula, como você viu no exemplo. Ao capturar uma exceção, você tem três opções:

  • Engula silenciosamente (manuseie e continue correndo).
  • Faça algo como logging, mas re-raise a mesma exceção para permitir que níveis mais altos lidem.
  • Gere uma exceção diferente em vez da original.

Engula a exceção

Você deve engolir a exceção se souber como lidar com ela e puder se recuperar totalmente.

Por exemplo, se você receber um arquivo de entrada que pode estar em formatos diferentes (JSON, YAML), tente analisá-lo usando analisadores diferentes. Se o analisador JSON gerou uma exceção de que o arquivo não é um arquivo JSON válido, você o engole e tenta com o analisador YAML. Se o analisador YAML também falhar, você permitirá que a exceção se propague.

Observe que outras exceções (por exemplo, arquivo não encontrado ou nenhuma permissão de leitura) serão propagadas e não serão capturadas pela cláusula except específica. Essa é uma boa política neste caso em que você deseja tentar a análise YAML somente se a análise JSON falhar devido a um problema de codificação JSON.

Se você quiser lidar tudo exceções, então basta usar except Exception. Por exemplo:

Observe que ao adicionar as evocê vincula o objeto de exceção ao nome e disponível em sua cláusula except.

Gerar novamente a mesma exceção

Para aumentar novamente, basta adicionar raise sem argumentos dentro do seu manipulador. Isso permite que você execute algum tratamento local, mas ainda permite que os níveis superiores também o manipulem. Aqui o invoke_function() A função imprime o tipo de exceção no console e, em seguida, gera novamente a exceção.

Gerar uma exceção diferente

Existem vários casos em que você deseja gerar uma exceção diferente. Às vezes, você deseja agrupar várias exceções de baixo nível diferentes em uma única categoria que é tratada uniformemente pelo código de nível superior. Em casos de pedidos, você precisa transformar a exceção no nível do usuário e fornecer algum contexto específico do aplicativo.

Cláusula Final

Às vezes, você deseja garantir que algum código de limpeza seja executado mesmo que uma exceção tenha sido gerada em algum lugar ao longo do caminho. Por exemplo, você pode ter uma conexão de banco de dados que deseja fechar quando terminar. Aqui está a maneira errada de fazer isso:

Se o query() função gera uma exceção, então a chamada para close_db_connection() nunca será executado e a conexão do banco de dados permanecerá aberta. o finally A cláusula sempre é executada após a execução de um manipulador de exceção try all. Aqui está como fazê-lo corretamente:

A chamada para open_db_connection() pode não retornar uma conexão ou gerar uma exceção em si. Nesse caso, não há necessidade de fechar a conexão do banco de dados.

Ao usar finallyvocê deve ter cuidado para não gerar nenhuma exceção, pois elas mascararão a exceção original.

Gerenciadores de contexto

Os gerenciadores de contexto fornecem outro mecanismo para agrupar recursos como arquivos ou conexões de banco de dados em código de limpeza que é executado automaticamente mesmo quando exceções são levantadas. Em vez de blocos try-finally, você usa o with declaração. Aqui está um exemplo com um arquivo:

Agora, mesmo se process() levantou uma exceção, o arquivo será fechado corretamente imediatamente quando o escopo do with bloco é encerrado, independentemente de a exceção ter sido tratada ou não.

Exploração madeireira

O registro em log é praticamente um requisito em sistemas não triviais e de longa duração. É especialmente útil em aplicativos da Web onde você pode tratar todas as exceções de maneira genérica: apenas registre a exceção e retorne uma mensagem de erro ao chamador.

Ao registrar, é útil registrar o tipo de exceção, a mensagem de erro e o rastreamento de pilha. Todas essas informações estão disponíveis através do sys.exc_info objeto, mas se você usar o logger.exception() método em seu manipulador de exceção, o sistema de log do Python extrairá todas as informações relevantes para você.

Esta é a melhor prática que eu recomendo:

Se você seguir esse padrão (supondo que você configurou o log corretamente), não importa o que aconteça, você terá um registro muito bom em seus logs do que deu errado e poderá corrigir o problema.

Se você aumentar novamente, certifique-se de não registrar a mesma exceção repetidamente em níveis diferentes. É um desperdício e pode confundir você e fazer você pensar que ocorreram várias instâncias do mesmo problema, quando na prática uma única instância foi registrada várias vezes.

A maneira mais simples de fazer isso é permitir que todas as exceções se propaguem (a menos que possam ser tratadas com confiança e engolidas antes) e, em seguida, fazer o log próximo ao nível superior de seu aplicativo/sistema.

Sentinela

O registro é uma capacidade. A implementação mais comum é usando arquivos de log. Mas, para sistemas distribuídos de grande escala com centenas, milhares ou mais servidores, esta nem sempre é a melhor solução.

Para acompanhar as exceções em toda a sua infraestrutura, um serviço como o sentry é super útil. Ele centraliza todos os relatórios de exceção e, além do stacktrace, adiciona o estado de cada quadro de pilha (o valor das variáveis ​​no momento em que a exceção foi gerada). Ele também fornece uma interface muito legal com painéis, relatórios e maneiras de dividir as mensagens por vários projetos. É de código aberto, para que você possa executar seu próprio servidor ou assinar a versão hospedada.

Lidando com falhas transitórias

Algumas falhas são temporárias, principalmente quando se trata de sistemas distribuídos. Um sistema que enlouquece ao primeiro sinal de problema não é muito útil.

Se o seu código estiver acessando algum sistema remoto que não está respondendo, a solução tradicional é o timeouts, mas às vezes nem todo sistema é projetado com timeouts. Os tempos limite nem sempre são fáceis de calibrar conforme as condições mudam.

Outra abordagem é falhar rapidamente e depois tentar novamente. O benefício é que, se o alvo estiver respondendo rapidamente, você não precisará passar muito tempo em condição de sono e poderá reagir imediatamente. Mas se falhar, você pode tentar novamente várias vezes até decidir que é realmente inacessível e gerar uma exceção. Na próxima seção, apresentarei um decorador que pode fazer isso por você.

Decoradores úteis

Dois decoradores que podem ajudar no tratamento de erros são os @log_errorque registra uma exceção e, em seguida, a eleva novamente, e o @retry decorador, que tentará chamar novamente uma função várias vezes.

Registrador de erros

Aqui está uma implementação simples. O decorador exclui um objeto logger. Quando ele decora uma função e a função é invocada, ele envolve a chamada em uma cláusula try-except e, se houver uma exceção, ele a registra e finalmente aumenta a exceção.

Aqui está como usá-lo:

Repetidor

Aqui está uma implementação muito boa do decorador @retry.

Conclusão

O tratamento de erros é crucial para usuários e desenvolvedores. O Python oferece ótimo suporte na linguagem e na biblioteca padrão para tratamento de erros baseado em exceção. Ao seguir as melhores práticas de forma diligente, você pode conquistar esse aspecto muitas vezes negligenciado.

Aprenda Python

Aprenda Python com nosso guia tutorial completo sobre Python, seja você apenas começando ou um programador experiente procurando aprender novas habilidades.

Deixe uma resposta