Python: Lidando com erros e exceções

Python: Lidando com erros e exceções
Python: Lidando com erros e exceções #cover

Programando em Python (ou, na verdade, em qualquer linguagem de programação!) você já deve ter se deparado com algumas mensagens de erros e exceções. Como podemos fazer nosso programa lidar com isso da melhor maneira?

Aqui na empresa, temos um programa em cada computador que registra, em um arquivo, as datas e os horários em que o computador foi ligado.

Recentemente, alteramos todo o registro de datas no padrão brasileiro (DD/MM/AAAA HH:mm) para o padrão ANSI (AAAA-MM-DD HH:mm:SS). Essa conversão de padrão foi feita utilizando geradores no Python.

Entretanto, começamos a usar um novo programa que exige o registro no padrão brasileiro, porque foi desenvolvido por uma empresa nacional. Nossa solução, então, foi ter dois registros com padrões diferentes para os programas diferentes que usamos, em vez de salvar em um padrão ou outro.

Dessa forma, antes de tudo precisávamos de um outro arquivo registros_br.txt com todos os registros já salvos no registros.txt, mas convertidos ao padrão brasileiro. Por enquanto, temos um arquivo registros.txt assim:


2018-04-30 10:05:00
Dia do Trabalho - FERIADO
2018-05-02 12:30:00
2018-05-03 11:00:00
2018-05-04 15:00:00
SÁBADO
DOMINGO
2018-05-07 09:20:00
...

Já sabemos como fazer esse tipo de conversão usando um gerador e a função strptime() do tipo datetime:


from datetime import datetime

registros_ansi = open('registros.txt', 'r')

def cria_gerador_registros(registros_ansi):
    for linha in registros_ansi:
        data_hora = datetime.strptime(linha, '%Y-%m-%d %H:%M:%S')
        data_hora_br = data_hora.strftime('%d/%m/%Y %H:%M')
        yield data_hora_br

    registros_ansi.close()

Legal! Mas dessa vez não queremos imprimir as conversões, não é verdade? Queremos apenas passá-las para nosso novo arquivo registros_br.txt'. E agora?

Escrevendo em nosso arquivo

Para gravarmos os dados em nosso arquivo, temos que abri-lo com um modo diferente de 'r', para leitura, o 'w', para escrita! Dessa forma, podemos usar o método write() para adicionarmos texto nele.

Pensando nisso, podemos, também, usar uma função comum em vez de um gerador, para converter tudo de uma vez e passar para o novo arquivo:


from datetime import datetime

registros_ansi = open('registros.txt', 'r')
registros_br = open('registros_br.txt', 'w')

def converte_datas(registros_ansi, registros_br):
    for linha in registros_ansi:
        data_hora = datetime.strptime(linha, '%Y-%m-%d %H:%M:%S\n')
        data_hora_br = data_hora.strftime('%d/%m/%Y %H:%M\n')
        registros_br.write(data_hora_br)

    registros_ansi.close()
    registros_br.close()

converte_datas(registros_ansi, registros_br)

Certo! Fui rodar esse código e olha o resultado:


Traceback (most recent call last):
  File "converte_data.py", line 14, in <module>
    converte_datas(registros_ansi, registros_br)
  File "converte_data.py", line 8, in converte_datas
    data_hora = datetime.strptime(linha, '%Y-%m-%d %H:%M:%S\n')
  File "/usr/lib64/python3.6/_strptime.py", line 565, in _strptime_datetime
    tt, fraction = _strptime(data_string, format)
  File "/usr/lib64/python3.6/_strptime.py", line 362, in _strptime
    (data_string, format))
ValueError: time data 'Dia do Trabalho - FERIADO\n' does not match format '%Y-%m-%d %H:%M:%S\n'_

Uma exceção gigante! Ela é do tipo ValueError e indica que a string 'Dia do Trabalho - FERIADO\n' não bate com a formatação que a gente passou '%Y-%m-%d %H:%M:%S\n'. Realmente não bate, não tem nada a ver… De alguma forma precisamos arrumar isso.

Banner da Escola de Programação: Matricula-se na escola de Programação. Junte-se a uma comunidade de mais de 500 mil estudantes. Na Alura você tem acesso a todos os cursos em uma única assinatura; tem novos lançamentos a cada semana; desafios práticos. Clique e saiba mais!

O problema do fechamento do arquivo

Antes de cuidar desse problema, porém, vamos checar como ficou o arquivo registros_br.txt, já que deu esse erro. Dei dois cliques no arquivo para abrir com o bloco de notas e olha o que aconteceu:

Erro de arquivo aberto no Windows

Outro erro! Dessa vez o erro foi do sistema operacional, mesmo, o Windows, que, por conta das configurações de meu sistema, está me impedindo de abrir o arquivo, indicando que ele já está aberto em python.exe.

Mas por que será será que esse erro ocorreu? Analisando nosso código, podemos ver que nem ao menos esquecemos de fechar nossos arquivos, como mostra essas linhas:


registros_ansi.close()
registros_br.close()

Então como o Python ainda mantém esse arquivo aberto? O que acontece é que essas linhas de nosso código, na verdade, não foram executadas! Mas como não foram executadas se elas estão lá na nossa função?!

Essas instruções de fato estão em nossa função converte_datas(), e o problema é justamente esse - a função não foi finalizada.

"Isso significa que a função ainda está sendo executada?"

Na verdade… não. A função foi encerrada, mas antes de chegar no final do código dela. E por quê? Por causa da exceção ValueError que tivemos! O Python não sabe como lidar com esse erro e simplesmente interrompe toda a execução do programa.

E agora? Bem, podemos pensar em alguma forma de evitar essa exceção, impedindo que isso acontecesse. Mas e se outro erro aparecesse? Novamente os arquivos ficariam abertos!

Seria bom se houvesse uma forma de garantir que as linhas que fecham os arquivos executassem, não importa o que aconteça… Mas como?

Garantindo a execução de um código com um bloco try/finally

Por enquanto, temos uma linha do código que é suscetível a erros, como já vimos:


data_hora = datetime.strptime(linha, '%Y-%m-%d %H:%M:%S\n')

Repare que o que queremos é tentar (try) executar essa linha e, independente do que aconteça, executar, finalmente (finally), as linhas de fechamento dos arquivos. No Python (e em diversas linguagens de programação), temos uma maneira especial de cuidar das coisas dessa forma - com o bloco try/finally. Sua estrutura é a seguinte:


try:
    data_hora = datetime.strptime(linha, '%Y-%m-%d %H:%M:%S\n')
    data_hora_br = data_hora.strftime('%d/%m/%Y %H:%M\n')
    registros_br.write(data_hora_br)
finally:
    registros_ansi.close()
    registros_br.close()

Vamos tentar executar nosso programa agora e ver o resultado:


# Mensagem de erro omitida

ValueError: time data 'Dia do Trabalho - FERIADO\n' does not match format '%Y-%m-%d %H:%M:%S\n'

Ok, o erro continua aparecendo, afinal não mexemos nisso, queríamos apenas garantir que os arquivos fossem fechados ao final. Fui tentar abrir o arquivo registros_br.txt e dessa vez… deu certo! Deu certo? Bem, conseguimos abrir, mas olha o conteúdo dele:


30/04/2018 10:05

Ué! Só salvou a primeira linha! Vamos analisar mais um pouco a mensagem de erro que recebemos:


ValueError: time data 'Dia do Trabalho - FERIADO\n' does not match format '%Y-%m-%d %H:%M:%S\n'

Repare que o erro indicado ocorreu com a string 'Dia do Trabalho - FERIADO\n'. Voltando lá em nosso arquivo registros.txt, vemos que essa linha era justamente a segunda!

Assim, a primeira linha passa pelo loop da maneira como queremos, mas logo a segunda já falha e interrompe todo o código, só executando o que vem no finally.

Precisamos, dessa vez, encontrar alguma maneira de contornar esse problema. Não quero remover essa linha do meu registro, pois nos ajuda a controlar as datas por aqui. Mantendo ela, o que podemos fazer?

Uma abordagem seria fazer uma verificação com if, checando se a linha é essa. Se for, queremos que ela fique igual no registros_br.txt. Vamos lá, então:


try:
    if linha == 'Dia do Trabalho - FERIADO\n':
        registros_br.write(linha)
    else:
        data_hora = datetime.strptime(linha, '%Y-%m-%d %H:%M:%S\n')
        data_hora_br = data_hora.strftime('%d/%m/%Y %H:%M\n')
        registros_br.write(data_hora_br)
finally:
    registros_ansi.close()
    registros_br.close()

Certo! Vamos executar novamente o nosso programa e ver o resultado:


# Mensagem de erro omitida

ValueError: time data 'SÁBADO\n' does not match format '%Y-%m-%d %H:%M:%S\n'

Outro ValueError, mas dessa vez com a linha 'SÁBADO\n'. Esquecemos de considerá-la na verificação. Olha como ficou nosso registros_br.txt:


30/04/2018 10:05
Dia do Trabalho - FERIADO
02/05/2018 12:30
03/05/2018 11:00
04/05/2018 15:00

Ainda incompleto… Podemos adicionar 'SÁBADO\n' em nossa verificação, também. Mas aí teríamos que adicionar 'DOMINGO\n'. E não poderíamos esquecer dos próximos feriados, como Corpus Christi, Independência do Brasil, e até o Natal!

Ficaria completamente inviável cobrir tudo isso com uma (ou mesmo várias) instruções if! Precisamos de uma maneira mais eficiente. Qual poderia ser a solução?

Tratando exceções com o bloco try/except

Uma outra abordagem que poderíamos seguir seria nos basear pela exceção, não pela linha. Ou seja, em vez de "se a linha for x, faça y", tratar como "se uma exceção acontecer, faça y". Quem já é mais familiarizado com programação, talvez saiba que isso tem um nome - tratamento de exceção ou exception handling.

Com base no que fizemos antes com o try/finally, vamos pensar no que de fato queremos que aconteça.

Em primeiro lugar, queremos tentar (try) executar aquela parte do código, exceto (except) nos casos em que ocorra um erro, em que queremos mandar direto a linha para o registros.txt. No nosso caso, depois de tudo (finally) ainda queremos fechar os dois arquivos que abrimos.

Assim, temos a estrutura básica do tratamento de exceções com o Python um bloco try/except, ou, no nosso caso, try/except/finally. Olha como montamos o código:


try:
    data_hora = datetime.strptime(linha, '%Y-%m-%d %H:%M:%S\n')
    data_hora_br = data_hora.strftime('%d/%m/%Y %H:%M\n')
    registros_br.write(data_hora_br)
except:
    registros_br.write(linha)
finally:
    registros_ansi.close()
    registros_br.close()

Legal! O único problema é que o except, da forma que está, vai capturar toda e qualquer exceção que aparecer.

"Mas, ué, não é isso que queremos?"

Será? Veja, as exceções que nos deparamos foram todas do tipo ValueError, indicando um problema na parte de formatação da data. Sabemos, então, que estamos falando, na verdade, desse único tipo de exceção, que é o esperado.

Da forma como está agora, erros mais graves, como a tentativa de fechamento do programa (KeyboardInterrupt) ou um problema na memória (MemoryError), passariam silenciosamente, sem sequer ficarmos sabendo. Como melhorar isso?

Capturando exceções específicas com except

No Python, ainda podemos, então, especificar a exceção que queremos tratar, mudando o except: para:


except ValueError:

Vamos rodar nosso programa e ver o que acontece. O código rodou sem erros e finalizou sua execução. Será que é um bom sinal? Vamos ver como ficou nosso arquivo registros_br.txt:


30/04/2018 10:05
Dia do Trabalho - FERIADO
02/05/2018 12:30
03/05/2018 11:00
04/05/2018 15:00
SÁBADO
DOMINGO
07/05/2018 09:20
...

Deu certo! Exatamente como queríamos, agora! O problema foi resolvido com sucesso, mas ainda há uma última coisa que me incomoda um pouco em nosso código…

Faz tempo que sabemos exatamente qual linha que estava resultando na exceção ValueError:


data_hora = datetime.strptime(linha, '%Y-%m-%d %H:%M:%S\n')

Ou seja, é apenas nessa linha que esperamos uma possibilidade de exceção. Entretanto, estamos englobando mais outras duas linhas, de formatação da data em uma string e de escrita no arquivo, no try.

Assim, o except vai procurar por um ValueError em qualquer uma dessas três linhas, quando na verdade só queremos na primeira, que é o que esperamos!

O ideal, então, seria deixar dentro do try apenas o trecho que sabemos que pode gerar uma exceção - a primeira linha. Mas se for assim, onde colocamos o resto do código?

"E se tudo der certo?" com a cláusula else

Vamos novamente organizar, então, o nosso objetivo geral. Queremos tentar (try) transformar uma string em datetime, exceto (except) ocorra um ValueError, em que queremos que a string seja simplesmente gravada no arquivo registros_br.txt.

Caso não (else) ocorra esse ValueError, queremos formatar a data no padrão brasileiro e então mandá-la para o arquivo de registro. Finalmente (finally), precisamos fechar os dois arquivos que abrimos.

Assim, podemos completar nosso bloco de tratamento de exceção com a cláusula else (sim, igual aquela do if/else!), no qual o código dentro dela será executado apenas se o except não capturar nenhuma exceção:


try:
    data_hora = datetime.strptime(linha, '%Y-%m-%d %H:%M:%S\n')
except ValueError:
    registros_br.write(linha)
else:
    data_hora_br = data_hora.strftime('%d/%m/%Y %H:%M\n')
    registros_br.write(data_hora_br)
finally:
    registros_ansi.close()
    registros_br.close()

E nosso código está ainda mais efetivo e prático!

Para saber mais: Gerenciadores de contexto

O tratamento de exceções se mostrou uma solução muito boa para os nossos problemas, mas sabia que há uma maneira mais simples de lidarmos com fechamentos de arquivos no Python? Com a palavra-chave with, podemos criar o que chamamos de gerenciador de contexto, que manterá o arquivo aberto apenas até o fim do determinado contexto criado:


with open('registros.txt', 'r') as registros_ansi, open('registros_br.txt', 'w') as registros_br:
    for linha in registros_ansi:
        ## código omitido
        registros_br.write(data_hora_br)

print(registros_ansi.closed())
print(registros_br.closed())

E olha o resultado quando rodamos o código:


True
True

Os dois arquivos foram fechados automaticamente, sem a necessidade da chamada do método close() por nossa parte!

Conclusão: Tratando erros com elegância

Nesse post, aprendemos sobre tratamento de exceções no Python, vendo como essa técnica pode resolver e simplificar muitos de nossos problemas, transformando erros em coisas menos assustadoras para nós, desenvolvedores!

Aprendemos que há diversas formas de estruturar esse tratamento, com blocos como try/finally, try/except, try/except/finally e ainda o mais completo - try/except/else/finally, em que conseguimos especificar com elegância o comportamento que queremos que nosso código tenha frente a uma exceção.

E aí, o que achou do tratamento de exceções no Python? Se gostou do conteúdo, que tal dar uma olhada também em nossos cursos de Python lá na Alura?

Veja outros artigos sobre Programação