sábado, 5 de janeiro de 2013

Tratamento de Exceções no Python

O Mundo Não é Perfeito; Muito Menos a Programação


Este é um assunto importante. Além de definir o que o seu programa faz, também é preciso deixar claro o que ele não faz e, assim, impedir que uma ação ilegal cause um erro fatal. É dever do programador antever o tipo de problemas que podem causar um erro no seu código e fazer o devido tratamento.
Mas o tratamento de exceções não serve apenas para evitar que o sistema seja interrompido. É uma oportunidade de se recuperar de um erro, é uma ferramenta de depuração em ambiente de testes, é uma forma de informar com clareza qual é o problema e como lidar com ele.

No Python: "Tente" Outra Vez


Abaixo temos um exemplo da sintaxe completa de tratamento de exceções no Python:

#Python 3
try:
    print("Eu posso provocar um erro")
except:
    print("Eu só apareço se houver um erro")
else:
    print("Eu só apareço se NÃO houver um erro")
finally:
    print("Eu SEMPRE apareço")
O bloco básico é o try-except. Dentro do try está o código passível de causar um erro. Caso um erro ocorra, o interpretador passará diretamente para o except. Vamos ver um código mais prático:

#Python 3
print("Início do programa")
try:
    print("Até agora tudo bem...")
    variavel_nao_declarada
    print("Isso aqui nunca vai aparecer")
except:
    print("Erro! Uma variável não foi declarada")
# O Programa segue normalmente
print("Fim do programa")
Ao executar o código acima recebemos esta saída:
>>>
Início do programa
Até agora tudo bem...
Erro! Uma variável não foi declarada
Fim do programa
>>> 
Agora um versão diferente:
#Python 3
print("Início do programa")
try:
    print("Até agora tudo bem...")
    variavel_declarada = 10
    print("Agora isso vai aparecer")
except:
    print("Erro! Uma variável não foi declarada")
# O Programa segue normalmente
print("Fim do programa")
E o resultado:
>>>
Início do programa
Até agora tudo bem...
Agora isso vai aparecer
Fim do programa
>>> 
Ou seja, o interpretador "tenta" executar o código dentro do try e se não houver nenhum problema ele pula o except e segue normalmente. Mas se houver algum erro ele interrompe a leitura no mesmo instante e passa para o except. Após executar o except ele também irá continuar normalmente.

Seja Específico


O que acontece se dentro do nosso try houver várias linhas de código que podem causar erros diferentes? O ideal é separar cada erro e tratá-lo de forma específica como no exemplo:
#Python 3
print("Início do programa")
try:    
    variavel_nao_declarada
    operacao_ilegal = 5 + "5"
    lista = ["um","dois","três"]
    item_nao_existe = lista[3]
    este_erro_nao_esta_previsto = 10 / 0
except NameError:
    print("Erro! Uma variável não foi declarada")
except TypeError:
    print("Erro! Não é possível somar um inteiro com uma string")
except IndexError:
    print("Erro! A lista não possui tantos itens")
except:
    print("Erro não previsto!")
# O Programa segue normalmente
print("Fim do programa")
Acima temos cinco linhas de código no bloco try, cada uma causa um erro diferente. Quatro delas são tratadas especificamente. Mesmo que você especifique todos os possíveis erros, sempre coloque um except geral no final para pegar qualquer outro erro não previsto.
Podemos melhorar mais o nosso código adicionando os detalhes que o próprio interpretador fornece. Desta forma, fica mais fácil para o programador depurar o erro:
#Python 3
print("Início do programa")
try:    
    variavel_nao_declarada    
except NameError as ne:
    print("Erro! Uma variável não foi declarada")
    print(ne)
print("Fim do programa")
O resultado:
>>>
Início do programa
Erro! Uma variável não foi declarada
name 'variavel_nao_declarada' is not defined
Fim do programa
>>> 
Também é possível agrupar erros diferentes no mesmo except:
#Python 3
def calcula_juros(valor1, valor2):
    try:
        return valor1 / valor2    
    except (ValueError, NameError, TypeError) as e:
        print("Erro! Verifique os argumentos")
        print(e)
    except:
        print("Erro inesperado")

print(calcula_juros(10,"5"))

Por Que else?


O bloco try-except também suporta o else. A função dele é exatamente o contrário do except, ou seja, ele só é executado quando nenhuma exceção ocorre dentro do try.
Imagine que eu tenha um módulo próprio que criei para realizar operações com valores matemáticos precisos. Agora, para utilizar este módulo eu também vou precisar importar o módulo padrão math do Python. Veja uma maneira de implementar essa ideia:
#Python 3
try:
    import meu_modulo
except ImportError as ie:
    print("Erro! Não foi possível importar o módulo")
    print(ie)
else:
    import math
    #chamar demais funções
O módulo math é uma dependência do meu_modulo, logo, se eu não conseguir importar o meu_modulo não preciso importá-lo. Como o meu_modulo foi criado por mim, existe uma possibilidade de que ele não possa ser importado caso eu não coloque ele no diretório correto ou execute o meu programa em um ambiente diferente. Já o módulo math vem incluído no Python por padrão e é muito mais improvável que eu não consiga importá-lo.
Mesmo assim, o uso do else no bloco try-except deve ser feito com muita atenção. Considere a seguinte alternativa:
#Python 3
try:
    import meu_modulo
    import math
except ImportError as ie:
    print("Erro! Não foi possível importar o módulo")
    print(ie)
Neste caso, se houver um erro de importação com o meu_modulo, o except cuidará disso da mesma forma como no exemplo anterior. Se não houver problema, o módulo math também será importado. A diferença é que se houver um erro ao importar o módulo math, mesmo que improvável, o except também está pronto para cuidar do problema, diferente do exemplo anterior.
É preciso ter em mente que quando utilizamos o else após um try-except não temos mais meios de prevenir um novo erro (a não ser que utilizemos um novo try-except).
O melhor é utilizar o else com reservas.

Finalmente, o finally


O finally é simples. O código dentro dele sempre é executado, ocorrendo um erro ou não. E o objetivo disso já está no próprio nome. Serve para finalizar operações que podem ter sido interrompidas. Uma utilidade muito comum é esta:
#Python 3
def pegar_dados():
    return ["linha1\n","linha2\n"]

try:
    arquivo = open("meu_arquivo.txt", "a")
    arquivo.write("novos dados:\n")
    dados = pegar_dados()
    arquivo.writelines(dados)
    arquivo.close()
except FileNotFoundError as fnfe:
    print("Erro! Arquivo não encontrado")
    print(fnfe)
except:
    print("Erro inesperado")
Note que, caso ocorra um erro durante a escrita dos dados no arquivo, o interpretador irá direto para o except sem ler as outras linhas do try. Ou seja, o arquivo não será fechado corretamente e isto pode resultar em dados corrompidos ou erros de execução futuros.
Neste caso, precisamos ter a certeza de que o arquivo será fechado com ou sem erro. E para isso serve o finally:
#Python 3
def pegar_dados():
    return ["linha1\n","linha2\n"]

try:
    arquivo = open("meu_arquivo.txt", "a")
    arquivo.write("novos dados:\n")
    dados = pegar_dados()
    arquivo.writelines(dados)    
except FileNotFoundError as fnfe:
    print("Erro! Arquivo não encontrado")
    print(fnfe)
except:
    print("Erro inesperado")
finally:
    arquivo.close()
É importante ressaltar que, no caso do tratamento de arquivos (ou qualquer objeto que suporte o protocolo de context management), o melhor é utilizar o comando with, pois estes objetos já incluem métodos que tratam do fechamento adequado de seu acesso, sem a necessidade do try-except-finally. O exemplo acima serve apenas para fins didáticos. Porém, o finally é muito útil para fechar conexões, sockets e qualquer outra operação que fique pendente em caso de erro. 

Criando Exceções


Até agora vimos como tratar exceções que o próprio Python gera quando encontra alguma inconsistência. Quando estamos criando módulos auxiliares que serão utilizados em outros programas ou em um nível superior da aplicação, devemos criar nossas próprias exceções para evitar que nosso código receba dados incorretos. 
No Python, criamos exceções com a palavra reservada raise. Vamos ver um exemplo simples de como fazer:

#Python 3
import datetime

def calcula_dias_passados(data):
    if not isinstance(data, datetime.date):
        raise TypeError("Data precisa ser do tipo datetime.date")
    elif data >= datetime.date.today():
        raise ValueError("Data deve ser anterior ao dia de hoje")
    else:
        dias = datetime.date.today() - data
        return dias.days
Assim damos a oportunidade para que outro programador (ou nós mesmos, em outro momento) trate o erro da forma correta.
Na documentação do Python, há uma lista com as exceções disponíveis.
Mesmo definindo todas as possíveis exceções na lógica do nosso módulo, é uma boa prática cercarmos o código para pegarmos exceções não previstas, adicionando mais informações e relançando-as novamente. Veja um exemplo:
#Python 3
import datetime

def calcula_dias_passados(data):
    if not isinstance(data, datetime.date):
        raise TypeError("Data precisa ser do tipo datetime.date")
    elif data >= datetime.date.today():
        raise ValueError("Data deve ser anterior ao dia de hoje")
    else:
        try:
            dias = datetime.date.today() - data
            return dias.days
        except:
            print("Erro inesperado na função calcula_dias_passados")
            raise

try:
    print(calcula_dias_passados(datetime.date(2011,8,12)))
except (TypeError, ValueError) as e:
    print("Erro nos argumentos passados")
    print(e)
except Exception as ex:
    print("Erro no módulo: "+str(ex))
Uma parte importante no tratamento de exceções é informar o problema com clareza. Neste artigo, os exemplos apenas imprimem mensagens na tela. Mas uma ferramenta mais útil em programas maiores é o log. O except é o local perfeito para adicionar dados no log sobre erros ocorridos. Veremos mais sobre logging em um artigo futuro.

Um comentário:

  1. Bom dia, sou iniciante em python e preciso criar uma aplicação usando try/finally. Por onde começo?

    ResponderExcluir