26. Técnicas de Programação Defensiva com Exceções e Validação
Programação defensiva (defensive programming) significa escrever código que antecipa problemas antes que eles aconteçam. Em vez de assumir que tudo vai funcionar perfeitamente, código defensivo valida entradas, trata erros de forma elegante e verifica suposições. Essa abordagem cria programas mais confiáveis, mais fáceis de depurar, e menos propensos a travar de forma inesperada.
Nos capítulos anteriores, aprendemos como lidar com exceções quando elas ocorrem. Agora vamos aprender como evitar que muitos erros aconteçam em primeiro lugar, e como pegar problemas cedo quando eles de fato ocorrem.
26.1) Validando Argumentos de Funções
Funções frequentemente recebem dados de outras partes do seu programa ou de usuários. Se uma função receber dados inválidos, ela pode produzir resultados incorretos, travar com um erro confuso, ou causar problemas em outras partes do seu programa. Validação de argumentos (argument validation) significa verificar se os argumentos da função atendem aos seus requisitos antes de usá-los.
26.1.1) Por que Validar Argumentos?
Considere esta função que calcula a porcentagem de nota de um estudante:
def calculate_percentage(points_earned, total_points):
return (points_earned / total_points) * 100
# Usando a função
percentage = calculate_percentage(85, 100)
print(f"Grade: {percentage}%") # Output: Grade: 85.0%Isso funciona bem com entradas válidas. Mas o que acontece com dados problemáticos?
# Problema 1: Divisão por zero
percentage = calculate_percentage(85, 0) # ZeroDivisionError!
# Problema 2: Valores negativos (não faz sentido)
percentage = calculate_percentage(-10, 100) # -10.0%
# Problema 3: Pontos obtidos excede o total (impossível)
percentage = calculate_percentage(120, 100) # 120.0%Sem validação, a função ou trava ou produz resultados sem sentido. As mensagens de erro não explicam o que deu errado do ponto de vista da lógica de negócio—elas só mostram falhas técnicas.
26.1.2) Validação Básica de Argumentos com Condicionais
A abordagem mais simples de validação usa instruções if para checar argumentos e lançar exceções quando eles são inválidos:
def calculate_percentage(points_earned, total_points):
# Validar total_points
if total_points <= 0:
raise ValueError("total_points must be positive")
# Validar points_earned
if points_earned < 0:
raise ValueError("points_earned cannot be negative")
if points_earned > total_points:
raise ValueError("points_earned cannot exceed total_points")
# Todas as validações passaram - é seguro calcular
return (points_earned / total_points) * 100
# Uso válido
percentage = calculate_percentage(85, 100)
print(f"Grade: {percentage}%") # Output: Grade: 85.0%
# Uso inválido - mensagens de erro claras
try:
percentage = calculate_percentage(85, 0)
except ValueError as e:
print(f"Error: {e}") # Output: Error: total_points must be positive
try:
percentage = calculate_percentage(-10, 100)
except ValueError as e:
print(f"Error: {e}") # Output: Error: points_earned cannot be negative
try:
percentage = calculate_percentage(120, 100)
except ValueError as e:
print(f"Error: {e}") # Output: Error: points_earned cannot exceed total_pointsAgora, quando algo dá errado, a mensagem de erro explica claramente qual é o problema e como corrigir.
26.1.3) Validando Tipos de Argumentos
Às vezes você precisa garantir que os argumentos são do tipo correto:
def calculate_discount(price, discount_percent):
# Validar tipos
if not isinstance(price, (int, float)):
raise TypeError("price must be a number")
if not isinstance(discount_percent, (int, float)):
raise TypeError("discount_percent must be a number")
# Validar valores
if price < 0:
raise ValueError("price cannot be negative")
if not (0 <= discount_percent <= 100):
raise ValueError("discount_percent must be between 0 and 100")
# Calcular desconto
discount_amount = price * (discount_percent / 100)
return price - discount_amount
# Uso válido
final_price = calculate_discount(50.00, 20)
print(f"Final price: ${final_price:.2f}") # Output: Final price: $40.00
# Erro de tipo
try:
final_price = calculate_discount("50", 20)
except TypeError as e:
print(f"Error: {e}") # Output: Error: price must be a number
# Erro de valor
try:
final_price = calculate_discount(50.00, 150)
except ValueError as e:
print(f"Error: {e}") # Output: Error: discount_percent must be between 0 and 100A função isinstance() verifica se um objeto é uma instância de um tipo ou tipos especificados. Passamos uma tupla (int, float) para aceitar tanto inteiros quanto floats, já que ambos são tipos numéricos válidos para preços.
Quando validar tipos: a filosofia do Python é “duck typing”—se um objeto se comporta como o que você precisa, use-o. Validação de tipo é mais útil quando:
- Você está escrevendo uma função que será usada por outras pessoas
- Erros de tipo causariam falhas confusas mais tarde
- A função faz parte de uma API pública ou biblioteca
26.1.4) Validando Argumentos de Coleções
Quando funções aceitam listas, dicionários, ou outras coleções, valide tanto a coleção quanto seu conteúdo:
def calculate_average_grade(grades):
# Validar a própria coleção
if not isinstance(grades, list):
raise TypeError("grades must be a list")
if len(grades) == 0:
raise ValueError("grades list cannot be empty")
# Validar cada nota dentro da coleção
for i, grade in enumerate(grades):
if not isinstance(grade, (int, float)):
raise TypeError(f"grade at index {i} must be a number, got {type(grade).__name__}")
if not (0 <= grade <= 100):
raise ValueError(f"grade at index {i} must be between 0 and 100, got {grade}")
# Todas as validações passaram
return sum(grades) / len(grades)
# Uso válido
grades = [85, 92, 78, 95]
average = calculate_average_grade(grades)
print(f"Average: {average:.1f}") # Output: Average: 87.5
# Erro de lista vazia
try:
average = calculate_average_grade([])
except ValueError as e:
print(f"Error: {e}") # Output: Error: grades list cannot be empty
# Tipo de nota inválido
try:
average = calculate_average_grade([85, "92", 78])
except TypeError as e:
print(f"Error: {e}") # Output: Error: grade at index 1 must be a number, got str
# Valor de nota inválido
try:
average = calculate_average_grade([85, 92, 150])
except ValueError as e:
print(f"Error: {e}") # Output: Error: grade at index 2 must be between 0 and 100, got 150Repare como incluímos o índice nas mensagens de erro ao validar elementos de coleções. Isso ajuda a identificar exatamente qual item está problemático, especialmente em coleções grandes.
26.2) Verificando se a Entrada do Usuário é Válida
Entrada do usuário é inerentemente pouco confiável—usuários cometem erros de digitação, interpretam instruções errado, ou digitam dados em formatos inesperados. Validar a entrada do usuário evita que esses enganos causem travamentos no programa ou resultados incorretos.
26.2.1) Padrão Básico de Validação de Entrada
O padrão fundamental para validação de entrada combina input() com verificações de validação:
# Obter entrada do usuário
age_str = input("Enter your age: ")
# Validar a entrada
try:
age = int(age_str)
if age < 0:
print("Error: Age cannot be negative")
elif age > 150:
print("Error: Age seems unrealistic")
else:
print(f"You are {age} years old")
except ValueError:
print("Error: Please enter a valid number")Esse padrão tem três partes:
- Obter a entrada como uma string
- Tentar convertê-la para o tipo necessário
- Verificar se o valor convertido é válido
Vamos ver isso em ação com entradas diferentes:
# Entrada válida
# Usuário digita: 25
# Saída: You are 25 years old
# Tipo inválido
# Usuário digita: twenty-five
# Saída: Error: Please enter a valid number
# Valor inválido (negativo)
# Usuário digita: -5
# Saída: Error: Age cannot be negative
# Valor inválido (irrealista)
# Usuário digita: 200
# Saída: Error: Age seems unrealistic26.2.2) Validando Intervalos e Formatos de Entrada
Algumas entradas precisam ficar dentro de intervalos específicos ou corresponder a formatos particulares:
# Validando um mês (1-12)
month_str = input("Enter month (1-12): ")
try:
month = int(month_str)
if not (1 <= month <= 12):
print("Error: Month must be between 1 and 12")
else:
print(f"Month: {month}")
except ValueError:
print("Error: Please enter a whole number")
# Validando formato de e-mail (checagem simples)
email = input("Enter email: ")
if '@' not in email or '.' not in email:
print("Error: Email must contain @ and .")
else:
print(f"Email: {email}")
# Validando entrada de sim/não
response = input("Continue? (yes/no): ").lower().strip()
if response not in ['yes', 'no', 'y', 'n']:
print("Error: Please answer yes or no")
else:
if response in ['yes', 'y']:
print("Continuing...")
else:
print("Stopping...")A validação de e-mail aqui é intencionalmente simples—ela só verifica uma estrutura básica. Validação real de e-mail é bem mais complexa e normalmente usa expressões regulares (que vamos aprender no Capítulo 39).
26.2.3) Fornecendo Mensagens de Erro Úteis
Boas mensagens de erro dizem ao usuário exatamente o que deu errado e como corrigir:
# Mensagem de erro ruim
password = input("Enter password: ")
if len(password) < 8:
print("Error: Invalid password") # Não ajuda!
# Mensagem de erro melhor
password = input("Enter password: ")
if len(password) < 8:
print("Error: Password must be at least 8 characters long")
print(f"Your password is only {len(password)} characters")
# Ainda melhor - explique todos os requisitos logo de cara
print("Password requirements:")
print("- At least 8 characters")
print("- Must contain at least one number")
password = input("Enter password: ")
# Verificar comprimento
if len(password) < 8:
print(f"Error: Password too short ({len(password)} characters)")
print("Password must be at least 8 characters")
# Verificar se tem dígito
elif not any(char.isdigit() for char in password):
print("Error: Password must contain at least one number")
else:
print("Password accepted")A função any() retorna True se algum elemento em um iterável for verdadeiro. Aqui, char.isdigit() verifica se cada caractere é um dígito, e any() nos diz se pelo menos um caractere passou no teste.
26.3) Combinando input(), Loops e try/except para um Tratamento Robusto de Entrada
Verificações únicas de validação são úteis, mas elas não lidam com erros persistentes do usuário. Se um usuário digitar dados inválidos, seu programa deveria dar a ele outra chance. Combinar loops com validação cria um tratamento robusto de entrada que continua perguntando até obter dados válidos.
26.3.1) O Padrão Básico de Loop de Entrada
O padrão fundamental usa um loop while que continua até que uma entrada válida seja recebida:
# Continue perguntando até obter uma idade válida
while True:
age_str = input("Enter your age: ")
try:
age = int(age_str)
if age < 0:
print("Error: Age cannot be negative. Please try again.")
elif age > 150:
print("Error: Age seems unrealistic. Please try again.")
else:
# Entrada válida - sair do loop
break
except ValueError:
print("Error: Please enter a valid number.")
print(f"You are {age} years old")Esse padrão tem vários elementos-chave:
while True:cria um loop infinito- A validação acontece dentro do loop
breaksai do loop quando a entrada é válida- Mensagens de erro incentivam o usuário a tentar novamente
Vamos ver como isso lida com várias entradas:
# Exemplo de interação:
# Enter your age: twenty
# Error: Please enter a valid number.
# Enter your age: -5
# Error: Age cannot be negative. Please try again.
# Enter your age: 25
# You are 25 years old26.3.2) Criando Funções de Entrada Reutilizáveis
Quando você precisa do mesmo tipo de entrada validada em vários lugares, crie uma função:
def get_positive_integer(prompt):
"""Continue perguntando até o usuário digitar um inteiro positivo."""
while True:
try:
value = int(input(prompt))
if value <= 0:
print("Error: Please enter a positive number.")
else:
return value
except ValueError:
print("Error: Please enter a valid whole number.")
def get_number_in_range(prompt, min_value, max_value):
"""Continue perguntando até o usuário digitar um número no intervalo especificado."""
while True:
try:
value = float(input(prompt))
if value < min_value or value > max_value:
print(f"Error: Please enter a number between {min_value} and {max_value}.")
else:
return value
except ValueError:
print("Error: Please enter a valid number.")
# Usando as funções
quantity = get_positive_integer("Enter quantity: ")
print(f"Quantity: {quantity}")
grade = get_number_in_range("Enter grade (0-100): ", 0, 100)
print(f"Grade: {grade}")
temperature = get_number_in_range("Enter temperature (-50 to 50): ", -50, 50)
print(f"Temperature: {temperature}°C")Essas funções encapsulam a lógica de validação, deixando seu código principal mais limpo e mais legível. Elas também garantem um comportamento de validação consistente em todo o seu programa.
26.4) Usando Asserções para Checagens de Invariantes em Tempo de Desenvolvimento
Asserções (assertions) são um tipo especial de checagem usada durante o desenvolvimento para verificar se as suposições do seu código estão corretas. Diferente da validação (que lida com erros esperados de usuários ou de dados externos), asserções pegam erros de programação—situações que nunca deveriam acontecer se seu código estiver correto.
26.4.1) O que São Asserções e Quando Usá-las
Uma asserção (assertion) é uma instrução que deveria ser sempre verdadeira em um ponto específico do seu código. Se ela for falsa, algo está fundamentalmente errado com a lógica do seu programa:
def calculate_average(numbers):
# Isso nunca deveria acontecer se a função for chamada corretamente
assert len(numbers) > 0, "numbers list cannot be empty"
return sum(numbers) / len(numbers)
# Uso correto
grades = [85, 90, 78]
average = calculate_average(grades)
print(f"Average: {average:.1f}") # Output: Average: 84.3
# Uso incorreto - dispara assertion
empty_list = []
average = calculate_average(empty_list) # AssertionError: numbers list cannot be emptyQuando uma asserção falha, o Python levanta um AssertionError com a sua mensagem. Isso para o programa imediatamente e mostra exatamente onde sua suposição foi violada.
Distinção-chave:
- Validação (usando
iferaise): para lidar com problemas esperados de usuários ou dados externos - Asserções: para capturar bugs de programação durante o desenvolvimento
# Validação - lida com erros esperados do usuário
def get_positive_number(prompt):
while True:
try:
value = float(input(prompt))
if value <= 0:
print("Error: Please enter a positive number.")
else:
return value
except ValueError:
print("Error: Please enter a valid number.")
# Assertion - captura erros de programação
def calculate_discount(price, discount_rate):
# Isso nunca deveria ser violado se o programa estiver escrito corretamente
assert price >= 0, "price should be non-negative"
assert 0 <= discount_rate <= 1, "discount_rate should be between 0 and 1"
return price * (1 - discount_rate)26.4.2) Verificando Pré-condições de Funções
Assertions são excelentes para verificar se as pré-condições (preconditions) (requisitos que precisam ser verdadeiros antes da execução da função) são atendidas:
def get_list_element(items, index):
"""Pega um elemento de uma lista no índice especificado."""
# Pré-condições
assert isinstance(items, list), "items must be a list"
assert isinstance(index, int), "index must be an integer"
assert 0 <= index < len(items), f"index {index} out of range for list of length {len(items)}"
return items[index]
# Uso correto
numbers = [10, 20, 30, 40]
value = get_list_element(numbers, 2)
print(f"Value: {value}") # Output: Value: 30
# Erro de programação - tipo errado
value = get_list_element("not a list", 0) # AssertionError: items must be a list
# Erro de programação - índice inválido
value = get_list_element(numbers, 10) # AssertionError: index 10 out of range for list of length 4Essas assertions ajudam a pegar bugs durante o desenvolvimento. Se você acidentalmente passar o tipo errado ou um índice inválido, a assertion imediatamente diz o que deu errado.
26.4.3) Verificando Pós-condições de Funções
Pós-condições (postconditions) são condições que precisam ser verdadeiras depois que uma função executa. Assertions podem verificar se sua função produziu resultados válidos:
def calculate_percentage(part, whole):
"""Calcula qual porcentagem 'part' é de 'whole'."""
# Pré-condições
assert whole > 0, "whole must be positive"
assert part >= 0, "part must be non-negative"
# Calcular porcentagem
percentage = (part / whole) * 100
# Pós-condição - o resultado deve ser uma porcentagem válida
assert 0 <= percentage <= 100, f"percentage {percentage} is outside valid range"
return percentage
# Isso funciona corretamente
percentage = calculate_percentage(25, 100)
print(f"Percentage: {percentage}%") # Output: Percentage: 25.0%
# Isso revela um erro de lógica na nossa função
# (a gente não verificou que part <= whole)
percentage = calculate_percentage(150, 100) # AssertionError: percentage 150.0 is outside valid rangeA assertion de pós-condição capturou um bug na nossa função—a gente esqueceu de validar que part não excede whole. Isso é exatamente para o que assertions servem: capturar erros de programação.
26.4.4) Assertions Podem ser Desativadas
Uma característica importante das assertions é que elas podem ser desativadas ao rodar Python com a flag -O (optimize):
# Este arquivo se chama test_assertions.py
def divide(a, b):
assert b != 0, "divisor cannot be zero"
return a / b
result = divide(10, 2)
print(f"Result: {result}")
result = divide(10, 0) # AssertionError quando assertions estão habilitadasExecutando normalmente:
python test_assertions.py
# Output: Result: 5.0
# Then: AssertionError: divisor cannot be zeroExecutando com otimização:
python -O test_assertions.py
# Output: Result: 5.0
# Then: ZeroDivisionError: division by zeroPor isso, assertions nunca devem ser usadas para validação de dados externos—se alguém rodar seu programa com -O, todas as assertions são puladas. Use assertions apenas para capturar bugs de programação durante desenvolvimento e testes.