Exceções E Erros
Voltando às contas que criamos no capítulo 8, o que aconteceria ao tentarmos chamar o método saca() com um valor fora do limite? O sistema mostraria uma mensagem de erro, mas quem chamou o método saca() não saberá que isso aconteceu.
Como avisar àquele que chamou o método de que ele não conseguiu fazer aquilo que deveria?
Os métodos dizem qual o contrato que eles devem seguir. Se, ao tentar chamar o método sacar() , ele não consegue fazer o que deveria, ele precisa, ao menos, avisar ao usuário que o saque não foi feito.
Veja no exemplo abaixo: estamos forçando uma Conta a ter um valor negativo, isto é, a estar em um estado inconsistente de acordo com a nossa modelagem.
conta = Conta(‘123-4’, ‘João’) conta.deposita(100.0) conta.saca(3000.0)
o método saca funcionou?
Em sistemas de verdade, é muito comum que quem saiba tratar o erro é aquele que chamou o método, e não a própria classe! Portanto, nada mais natural sinalizar que um erro ocorreu.
A solução mais simples utilizada antigamente é a de marcar o retorno de um método como boolean e retornar True se tudo ocorreu da maneira planejada, ou False , caso contrário:
if (valor > self.saldo + self.limite): print("nao posso sacar fora do limite") return False
java
else:
self.saldo -= valor return True
Um novo exemplo de chamada do método acima:
conta = Conta('123-4', 'João') conta.deposita(100.0) conta.limite = 100.0
if (not conta.saca(3000.0)): print(“nao saquei”)
Repare que tivemos de lembrar de testar o retorno do método, mas não somos obrigados a fazer isso. Esquecer de testar o retorno desse método teria consequências drásticas: a máquina de autoatendimento
poderia vir a liberar a quantia desejada de dinheiro, mesmo se o sistema não tivesse conseguido efetuar o método saca() com sucesso, como no exemplo a seguir:
conta = Conta(“123-4”, “João”) conta.deposita(100.0)
…
valor = 5000.0
conta.saca(valor) # vai retornar False, mas ninguém verifica caixa_eletronico.emite(valor)
Mesmo invocando o método e tratando o retorno de maneira correta, o que faríamos se fosse necessário sinalizar quando o usuário passou um valor negativo como valor? Uma solução seria alterar o retorno de boolean para int e retornar o código do erro que ocorreu. Isso é considerado uma má prática (conhecida também como uso de “magic numbers”).
Além de você perder o retorno do método, o valor devolvido é “mágico” e só legível perante extensa documentação, além de não obrigar o programador a tratar esse retorno e, no caso de esquecer isso, seu programa continuará rodando já num estado inconsistente.
Por esses e outros motivos, utilizamos um código diferente para tratar aquilo que chamamos de exceções: os casos onde acontece algo que, normalmente, não iria acontecer. O exemplo do argumento do saque inválido ou do id inválido de um cliente é uma exceção à regra.
Uma exceção representa uma situação que normalmente não ocorre e representa algo de estranho ou inesperado no sistema.
Antes de resolvermos o nosso problema, vamos ver como o interpretador age ao se deparar com situações inesperadas, como divisão por zero ou acesso a um índice de uma lista que não existe.
Para aprendermos os conceitos básicos das exceptions do Python, crie um arquivo teste_erro.py e teste o seguinte código você mesmo:
from conta import ContaCorrente
python
def metodo1():
python
print('início do metodo1') metodo2()
python
print('fim do metodo1')
python
def metodo2():
python
print('início do metodo2')cc = ContaCorrente(‘José’, ‘123’) for i in range(1,15):
cc.deposita(i + 1000) print(cc.saldo)
if(i == 5):
cc = None print(‘fim do metodo2’)
if name == ' main ': print('início do main') metodo1()
python
print('fim do main')Repare que durante a execução do programa chamamos o metodo1() e esse, por sua vez, chama o metodo2() . Cada um desses métodos pode ter suas próprias variáveis locais, isto é: o metodo1() não enxerga as variáveis declaradas dentro do executável e por aí em diante.
Como o Python (e muitas outras linguagens) faz isso? Toda invocação de método é empilhado em uma estrutura de dados que isola a área e memória de cada um. Quando um método termina (retorna), ele volta para o método que o invocou. Ele descobre isso através da pilha de execução (stack): basta remover o marcador que está no topo da pilha:
Porém, o nosso metodo2() propositalmente possui um enorme problema: está acessando uma referência para None quando o índice for igual a 6!
Rode o código. Qual a saída? O que isso representa? O que ela indica?
Essa saída é o rastro de pilha, o Traceback. É uma saída importantíssima para o programador - tanto que, em qualquer fórum ou lista de discussão, é comum os programadores enviarem, juntamente com a descrição do problema, essa Traceback. Mas por que isso aconteceu?
O sistema de exceções do Python funciona da seguinte maneira: quando uma exceção é lançada (raise), o interpretador entra em estado de alerta e vai ver se o método atual toma alguma precaução ao tentar executar esse trecho de código. Como podemos ver, o metodo2() não toma nenhuma medida diferente do que vimos até agora.
Como o metodo2() não está tratando esse problema, o interpretador para a execução dele anormalmente, sem esperar ele terminar, e volta um stackframe para baixo, onde será feita nova verificação: “o método1() está se precavendo de um problema chamado AttributeError ?” Se a resposta é não, ele volta para o executável, onde também não há proteção, e o interpretador morre.
Obviamente, aqui estamos forçando esse caso e não faria sentido tomarmos cuidado com ele. É fácil arrumar um problema desses: basta verificar antes de chamar os métodos se a variável está com referência para None .
Porém, apenas para entender o controle de fluxo de uma Exception, vamos colocar o código que vai tentar (try) executar um bloco perigoso e, caso o problema seja do tipo AttributeError , ele será excluído(except). Repare que é interessante que cada exceção no Python tenha um tipo, afinal ela pode ter atributos e métodos.
Adicione um try/except em volta do for , ‘pegando’ um AttributeError . O que o código imprime?
from conta import ContaCorrente
python
def metodo1():
python
print('início do metodo1') metodo2()
python
print('fim do metodo1')
python
def metodo2():
python
print('início do metodo2')cc = ContaCorrente(‘José’, ‘123’) try:
for i in range(1,15): cc.deposita(i + 1000) print(cc.saldo)
python
if (i == 5): cc = None
python
except:
java
print('erro') print('fim do metodo2')
python
if name == ' main ': print('início do main') metodo1()
python
print('fim do main')Ao invés de fazer o try em torno do for inteiro, tente apenas com o bloco dentro do for :
def metodo2():
python
print('início do metodo2')cc = ContaCorrente(‘José’, ‘123’)
for i in range(1,15): try:cc.deposita(i + 1000) print(cc.saldo)
if (i == 5):cc = None
except:
java
print('erro') print('fim do metodo2')Qual a diferença?
Retire o try/except e coloque ele em volta da chamada do metodo2() :
def metodo1():
python
print('início do metodo1') try:metodo2()
except AttributeError: print('erro')
java
print('fim do metodo1')Faça o mesmo, retirando otry/except novamente e colocando-o em volta da chamada do
metodo1() . Rode os códigos, o que acontece?
if name == ' main ': print('início do main') try:metodo1()
except AttributeError: print('erro')
java
print('fim do main')Repare que, a partir do momento que uma exception foi catched (pega, tratada, handled), a exceção volta ao normal a partir daquele ponto.