26. Tecniche di programmazione difensiva con eccezioni e validazione
La programmazione difensiva (defensive programming) significa scrivere codice che anticipa i problemi prima che si verifichino. Invece di dare per scontato che tutto funzioni perfettamente, il codice difensivo valida gli input, gestisce gli errori in modo elegante e verifica le assunzioni. Questo approccio crea programmi più affidabili, più facili da debuggare e meno inclini a bloccarsi inaspettatamente.
Nei capitoli precedenti, abbiamo imparato come gestire le eccezioni quando si verificano. Ora impareremo come prevenire che molti errori accadano in primo luogo e come intercettare i problemi in anticipo quando si verificano.
26.1) Validare gli argomenti delle funzioni
Le funzioni ricevono spesso dati da altre parti del programma o dagli utenti. Se una funzione riceve dati non validi, potrebbe produrre risultati errati, andare in crash con un errore confuso o causare problemi altrove nel programma. La validazione degli argomenti (argument validation) significa verificare che gli argomenti di una funzione soddisfino i requisiti prima di usarli.
26.1.1) Perché validare gli argomenti?
Considera questa funzione che calcola la percentuale del voto di uno studente:
def calculate_percentage(points_earned, total_points):
return (points_earned / total_points) * 100
# Utilizzo della funzione
percentage = calculate_percentage(85, 100)
print(f"Grade: {percentage}%") # Output: Grade: 85.0%Questo funziona bene con input validi. Ma cosa succede con dati problematici?
# Problema 1: Divisione per zero
percentage = calculate_percentage(85, 0) # ZeroDivisionError!
# Problema 2: Valori negativi (non ha senso)
percentage = calculate_percentage(-10, 100) # -10.0%
# Problema 3: I punti ottenuti superano il totale (impossibile)
percentage = calculate_percentage(120, 100) # 120.0%Senza validazione, la funzione o va in crash o produce risultati insensati. I messaggi di errore non spiegano cosa è andato storto dal punto di vista della logica di business: mostrano solo fallimenti tecnici.
26.1.2) Validazione di base degli argomenti con condizionali
L’approccio di validazione più semplice usa istruzioni if per controllare gli argomenti e sollevare eccezioni quando non sono validi:
def calculate_percentage(points_earned, total_points):
# Valida total_points
if total_points <= 0:
raise ValueError("total_points must be positive")
# Valida 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")
# Tutte le validazioni sono passate: si può calcolare in sicurezza
return (points_earned / total_points) * 100
# Utilizzo valido
percentage = calculate_percentage(85, 100)
print(f"Grade: {percentage}%") # Output: Grade: 85.0%
# Utilizzo non valido - messaggi di errore chiari
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_pointsOra, quando qualcosa va storto, il messaggio di errore spiega chiaramente qual è il problema e come risolverlo.
26.1.3) Validare i tipi degli argomenti
A volte devi assicurarti che gli argomenti siano del tipo corretto:
def calculate_discount(price, discount_percent):
# Valida i tipi
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")
# Valida i valori
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")
# Calcola lo sconto
discount_amount = price * (discount_percent / 100)
return price - discount_amount
# Utilizzo valido
final_price = calculate_discount(50.00, 20)
print(f"Final price: ${final_price:.2f}") # Output: Final price: $40.00
# Errore di tipo
try:
final_price = calculate_discount("50", 20)
except TypeError as e:
print(f"Error: {e}") # Output: Error: price must be a number
# Errore di valore
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 100La funzione isinstance() controlla se un oggetto è un’istanza di un tipo (o tipi) specificato. Passiamo una tupla (int, float) per accettare sia interi sia float, dato che entrambi sono tipi numerici validi per i prezzi.
Quando validare i tipi: la filosofia di Python è il "duck typing": se un oggetto si comporta come ti serve, usalo. La validazione dei tipi è più utile quando:
- Stai scrivendo una funzione che verrà usata da altri
- Errori di tipo causerebbero fallimenti confusi più avanti
- La funzione fa parte di una API pubblica o di una libreria
26.1.4) Validare gli argomenti delle collezioni
Quando le funzioni accettano liste(list), dizionari o altre collezioni, valida sia la collezione sia i suoi contenuti:
def calculate_average_grade(grades):
# Valida la collezione in sé
if not isinstance(grades, list):
raise TypeError("grades must be a list")
if len(grades) == 0:
raise ValueError("grades list cannot be empty")
# Valida ogni voto nella collezione
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}")
# Tutte le validazioni sono passate
return sum(grades) / len(grades)
# Utilizzo valido
grades = [85, 92, 78, 95]
average = calculate_average_grade(grades)
print(f"Average: {average:.1f}") # Output: Average: 87.5
# Errore: lista vuota
try:
average = calculate_average_grade([])
except ValueError as e:
print(f"Error: {e}") # Output: Error: grades list cannot be empty
# Tipo di voto non valido
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
# Valore del voto non valido
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 150Nota come includiamo l’indice nei messaggi di errore quando validiamo gli elementi di una collezione. Questo aiuta a identificare esattamente quale elemento è problematico, specialmente in collezioni grandi.
26.2) Verificare la validità dell’input dell’utente
L’input dell’utente è intrinsecamente inaffidabile: gli utenti fanno errori di battitura, fraintendono le istruzioni o inseriscono dati in formati inaspettati. Validare l’input dell’utente impedisce che questi errori causino crash del programma o risultati errati.
26.2.1) Schema di validazione di base dell’input
Lo schema fondamentale per la validazione dell’input combina input() con controlli di validazione:
# Ottieni l'input dell'utente
age_str = input("Enter your age: ")
# Valida l'input
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")Questo schema ha tre parti:
- Ottieni l’input come stringa
- Prova a convertirlo nel tipo necessario
- Controlla se il valore convertito è valido
Vediamo questo in azione con diversi input:
# Esempio di input valido
# User enters: 25
# Output: You are 25 years old
# Tipo non valido
# User enters: twenty-five
# Output: Error: Please enter a valid number
# Valore non valido (negativo)
# User enters: -5
# Output: Error: Age cannot be negative
# Valore non valido (irrealistico)
# User enters: 200
# Output: Error: Age seems unrealistic26.2.2) Validare intervalli e formati dell’input
Alcuni input devono rientrare in intervalli specifici o rispettare formati particolari:
# Validare un mese (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")
# Validare il formato dell'email (controllo semplice)
email = input("Enter email: ")
if '@' not in email or '.' not in email:
print("Error: Email must contain @ and .")
else:
print(f"Email: {email}")
# Validare input sì/no
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...")La validazione dell’email qui è intenzionalmente semplice: controlla solo la struttura di base. La validazione reale delle email è molto più complessa e in genere usa espressioni regolari (che impareremo nel Capitolo 39).
26.2.3) Fornire messaggi di errore utili
Buoni messaggi di errore dicono agli utenti esattamente cosa è andato storto e come risolverlo:
# Messaggio di errore scarso
password = input("Enter password: ")
if len(password) < 8:
print("Error: Invalid password") # Not helpful!
# Messaggio di errore migliore
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")
# Ancora meglio - spiega tutti i requisiti in anticipo
print("Password requirements:")
print("- At least 8 characters")
print("- Must contain at least one number")
password = input("Enter password: ")
# Controlla la lunghezza
if len(password) < 8:
print(f"Error: Password too short ({len(password)} characters)")
print("Password must be at least 8 characters")
# Controlla la presenza di una cifra
elif not any(char.isdigit() for char in password):
print("Error: Password must contain at least one number")
else:
print("Password accepted")La funzione any() restituisce True se almeno un elemento in un iterabile è vero. Qui, char.isdigit() controlla se ciascun carattere è una cifra e any() ci dice se almeno un carattere ha superato il test.
26.3) Combinare input(), cicli e try/except per una gestione robusta dell’input
I singoli controlli di validazione sono utili, ma non gestiscono gli errori persistenti degli utenti. Se un utente inserisce dati non validi, il programma dovrebbe dargli un’altra possibilità. Combinare i cicli(loop) con la validazione crea una gestione robusta dell’input che continua a chiedere finché non ottiene dati validi.
26.3.1) Lo schema base del ciclo di input
Lo schema fondamentale usa un ciclo while che continua fino a quando non viene ricevuto un input valido:
# Continua a chiedere finché non otteniamo un'età valida
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:
# Input valido: esci dal ciclo
break
except ValueError:
print("Error: Please enter a valid number.")
print(f"You are {age} years old")Questo schema ha diversi elementi chiave:
while True:crea un ciclo infinito- La validazione avviene all’interno del ciclo
breakesce dal ciclo quando l’input è valido- I messaggi di errore incoraggiano l’utente a riprovare
Vediamo come gestisce vari input:
# Esempio di interazione:
# 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) Creare funzioni riutilizzabili per l’input
Quando ti serve lo stesso tipo di input validato in più punti, crea una funzione:
def get_positive_integer(prompt):
"""Continua a chiedere finché l'utente non inserisce un intero 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):
"""Continua a chiedere finché l'utente non inserisce un numero nell'intervallo specificato."""
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.")
# Utilizzare le funzioni
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")Queste funzioni incapsulano la logica di validazione, rendendo il codice principale più pulito e leggibile. Inoltre garantiscono un comportamento di validazione coerente in tutto il programma.
26.4) Usare le asserzioni per controlli di invarianza in fase di sviluppo
Le asserzioni (assertions) sono un tipo speciale di controllo usato durante lo sviluppo per verificare che le assunzioni del codice siano corrette. A differenza della validazione (che gestisce errori attesi dagli utenti o da dati esterni), le asserzioni intercettano errori di programmazione: situazioni che non dovrebbero mai accadere se il codice è corretto.
26.4.1) Cosa sono le asserzioni e quando usarle
Un’asserzione (assertion) è una condizione che dovrebbe essere sempre vera in un punto specifico del codice. Se la condizione è falsa, c’è qualcosa di fondamentalmente sbagliato nella logica del programma:
def calculate_average(numbers):
# Questo non dovrebbe mai accadere se la funzione viene chiamata correttamente
assert len(numbers) > 0, "numbers list cannot be empty"
return sum(numbers) / len(numbers)
# Utilizzo corretto
grades = [85, 90, 78]
average = calculate_average(grades)
print(f"Average: {average:.1f}") # Output: Average: 84.3
# Utilizzo scorretto - attiva l'assert
empty_list = []
average = calculate_average(empty_list) # AssertionError: numbers list cannot be emptyQuando un assert fallisce, Python solleva un AssertionError con il tuo messaggio. Questo ferma immediatamente il programma e ti mostra esattamente dove è stata violata l’assunzione.
Distinzione chiave:
- Validazione (usando
iferaise): per gestire problemi attesi dagli utenti o da dati esterni - Asserzioni: per intercettare bug di programmazione durante lo sviluppo
# Validazione - gestisce errori utente attesi
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.")
# Assert - intercetta errori di programmazione
def calculate_discount(price, discount_rate):
# Queste condizioni non dovrebbero mai essere violate se il programma è scritto correttamente
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) Verificare le precondizioni delle funzioni
Gli assert sono eccellenti per verificare che le precondizioni (requisiti che devono essere veri prima che la funzione venga eseguita) siano rispettate:
def get_list_element(items, index):
"""Ottieni un elemento da una lista all'indice specificato."""
# Precondizioni
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]
# Utilizzo corretto
numbers = [10, 20, 30, 40]
value = get_list_element(numbers, 2)
print(f"Value: {value}") # Output: Value: 30
# Errore di programmazione - tipo errato
value = get_list_element("not a list", 0) # AssertionError: items must be a list
# Errore di programmazione - indice non valido
value = get_list_element(numbers, 10) # AssertionError: index 10 out of range for list of length 4Questi assert aiutano a intercettare bug durante lo sviluppo. Se per errore passi il tipo sbagliato o un indice non valido, l’assert ti dice immediatamente cosa è andato storto.
26.4.3) Verificare le postcondizioni delle funzioni
Le postcondizioni (postconditions) sono condizioni che devono essere vere dopo l’esecuzione di una funzione. Gli assert possono verificare che la funzione abbia prodotto risultati validi:
def calculate_percentage(part, whole):
"""Calcola quale percentuale 'part' rappresenta rispetto a 'whole'."""
# Precondizioni
assert whole > 0, "whole must be positive"
assert part >= 0, "part must be non-negative"
# Calcola la percentuale
percentage = (part / whole) * 100
# Postcondizione - il risultato dovrebbe essere una percentuale valida
assert 0 <= percentage <= 100, f"percentage {percentage} is outside valid range"
return percentage
# Questo funziona correttamente
percentage = calculate_percentage(25, 100)
print(f"Percentage: {percentage}%") # Output: Percentage: 25.0%
# Questo rivela un errore logico nella nostra funzione
# (non abbiamo controllato che part <= whole)
percentage = calculate_percentage(150, 100) # AssertionError: percentage 150.0 is outside valid rangeL’assert di postcondizione ha intercettato un bug nella nostra funzione: abbiamo dimenticato di validare che part non superi whole. Questo è esattamente a cosa servono le asserzioni: intercettare i bug di programmazione.
26.4.4) Le asserzioni possono essere disabilitate
Una caratteristica importante delle asserzioni è che possono essere disabilitate quando si esegue Python con il flag -O (optimize):
# Questo file si chiama 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 when assertions are enabledEsecuzione normale:
python test_assertions.py
# Output: Result: 5.0
# Then: AssertionError: divisor cannot be zeroEsecuzione con ottimizzazione:
python -O test_assertions.py
# Output: Result: 5.0
# Then: ZeroDivisionError: division by zeroEcco perché le asserzioni non dovrebbero mai essere usate per la validazione di dati esterni: se qualcuno esegue il programma con -O, tutte le asserzioni vengono saltate. Usa le asserzioni solo per intercettare bug di programmazione durante lo sviluppo e il test.