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.


⬅️ Capítulo Anterior | Próximo Capítulo ➡️