Capítulo 23, Básico
Exceções
Erros vão acontecer: arquivo ausente, rede fora, entrada inválida. O que define um bom programa é o que ele faz quando acontecem.
Código deste capítulo: basico/cap23_excecoes.py
Exceções são objetos organizados em hierarquia
Um erro em tempo de execução é representado por uma exceção, que é uma instância de uma classe. Todas descendem de BaseException, e as que você trata no dia a dia descendem de Exception:
print([c.__name__ for c in ZeroDivisionError.__mro__])
['ZeroDivisionError', 'ArithmeticError', 'Exception', 'BaseException', 'object']
Isso importa porque except ArithmeticError captura ZeroDivisionError também: capturar uma classe captura as filhas. Até os erros de sintaxe fazem parte da hierarquia (SyntaxError descende de Exception), embora apareçam na compilação, antes de o programa rodar.
try, except, else e finally
def converter(texto):
try:
valor = int(texto)
except ValueError:
print(f"'{texto}' não é um inteiro")
return None
else:
print("conversão feita")
return valor
finally:
print("fim da tentativa")
print(converter("42"))
print(converter("abc"))
conversão feita
fim da tentativa
42
'abc' não é um inteiro
fim da tentativa
None
try: o código que pode falhar.except: o que fazer se aquela exceção ocorrer.else: roda só se não houve exceção.finally: roda sempre, mesmo comreturnou erro. É o lugar da limpeza.
Capture apenas o que você sabe tratar
Eu sigo três regras. Primeiro: capture exceções específicas, nunca um except: vazio, que engole até o Ctrl+C. Segundo: o bloco try deve ser o menor possível. Terceiro: quando você precisar traduzir uma exceção, use raise ... from, que preserva a causa original:
def dividir(a, b):
try:
return a / b
except ZeroDivisionError:
return float("inf")
except TypeError as erro:
raise ValueError("os dois valores devem ser números") from erro
print(dividir(1, 0))
try:
dividir(1, "a")
except ValueError as erro:
print(erro, "|", type(erro.__cause__).__name__)
inf
os dois valores devem ser números | TypeError
Exceções próprias
Quando o seu domínio tem uma falha com nome (saldo insuficiente, pedido inexistente), crie uma classe. Quem usa seu código captura pelo nome, sem depender de texto de mensagem. As classes são o assunto do nível intermediário, então por enquanto copie o molde:
class SaldoInsuficiente(Exception):
def __init__(self, saldo, valor):
super().__init__(f"saldo {saldo} insuficiente para sacar {valor}")
self.saldo = saldo
self.valor = valor
def sacar(saldo, valor):
if valor > saldo:
raise SaldoInsuficiente(saldo, valor)
return saldo - valor
try:
sacar(100, 150)
except SaldoInsuficiente as erro:
print(erro, erro.valor - erro.saldo)
saldo 100 insuficiente para sacar 150 50
Pedir perdão ou pedir licença
Há dois estilos. LBYL (look before you leap) verifica antes de agir. EAFP (easier to ask forgiveness than permission) tenta e trata a falha. O estilo EAFP é o mais comum em Python, e é mais seguro em cenários concorrentes, porque entre "verificar" e "agir" o mundo pode mudar:
config = {"porta": 8080}
if "host" in config:
host = config["host"]
else:
host = "localhost"
try:
host = config["host"]
except KeyError:
host = "localhost"
print(host)
localhost
assert não é validação de entrada
O
assertserve para declarar o que precisa ser verdade no seu código, como uma checagem de sanidade para quem desenvolve. Ele pode ser desligado com a opção-Odo interpretador. Para validar dados de fora (entrada de usuário, arquivos, rede), useiferaise.
Exercício 1
Divisão segura
Escreva dividir_seguro(a, b) que devolva None quando b for zero.
Exercício 2
Validar uma idade
Escreva ler_idade(texto) que devolva um inteiro e levante ValueError com mensagem clara se o texto não for número ou se o valor for negativo.