Python raise ed Eccezioni Personalizzate
Impara a usare l'istruzione raise di Python, il concatenamento di eccezioni con raise...from e la creazione di classi di eccezione personalizzate.
Python ti permette di fare molto più che intercettare gli errori — puoi anche segnalarli deliberatamente con l'istruzione raise e creare i tuoi tipi di eccezione per rappresentare problemi specifici del dominio. Questo capitolo si basa su Python Try...Except e tratta:
- L'istruzione
raise— sollevare eccezioni built-in - Risollevare eccezioni all'interno di un blocco
except - Il concatenamento di eccezioni con
raise ... from - Creare classi di eccezione personalizzate
- Costruire una gerarchia di eccezioni per un'applicazione reale
- L'istruzione
asserte quando usarla
L'istruzione raise
L'istruzione raise ti consente di lanciare un'eccezione in qualsiasi punto del codice. La forma più comune passa un'istanza di eccezione con un messaggio descrittivo:
raise ExceptionType("message")Usa raise quando il tuo codice rileva un problema che il chiamante deve gestire. Ad esempio, una funzione che accetta un'età dovrebbe rifiutare immediatamente i valori negativi invece di proseguire silenziosamente:
def set_age(age):
if age < 0:
raise ValueError("Age cannot be negative")
return age
try:
set_age(-1)
except ValueError as e:
print(e)
# Output: Age cannot be negativeScegliere la Giusta Eccezione Built-in
I tipi di eccezione built-in di Python trasmettono un significato preciso. Scegliere quello corretto rende la tua API più comprensibile e consente ai chiamanti di gestire separatamente le diverse categorie di errore.
| Eccezione | Quando sollevarla |
|---|---|
ValueError | L'argomento ha il tipo corretto ma un valore non valido (age = -1) |
TypeError | L'argomento ha il tipo sbagliato (age = "old") |
KeyError | Manca una chiave obbligatoria del dizionario |
IndexError | L'indice di una sequenza è fuori intervallo |
FileNotFoundError | Un file richiesto non esiste |
PermissionError | Il processo non ha i diritti per eseguire un'operazione |
RuntimeError | Un problema generico di runtime che non rientra in un tipo più specifico |
NotImplementedError | Un metodo esiste in una classe base ma deve essere sovrascritto |
Sollevare ValueError per un valore errato è molto più informativo che sollevare una generica Exception, perché i chiamanti possono scrivere except ValueError per gestire esattamente quel caso.
Risollevare un'Eccezione
A volte vuoi fare qualcosa quando si verifica un'eccezione — registrarla, liberare una risorsa — e poi lasciare che la stessa eccezione si propaghi al chiamante senza modifiche. Chiama raise senza argomenti all'interno di un blocco except per risollevare l'eccezione corrente:
def read_config(path):
try:
with open(path) as f:
return f.read()
except FileNotFoundError:
print(f"Warning: config file not found at {path}")
raise # re-raise the original FileNotFoundError
try:
read_config("missing.cfg")
except FileNotFoundError as e:
print(f"Caught: {e}")
# Output:
# Warning: config file not found at missing.cfg
# Caught: [Errno 2] No such file or directory: 'missing.cfg'Usare raise senza argomenti preserva il traceback originale, il che rende il debug molto più semplice rispetto all'intercettare e risollevare e come una nuova eccezione.
Concatenamento di Eccezioni con raise ... from
Quando intercetti un'eccezione e ne sollevi un'altra differente, Python registra automaticamente l'eccezione originale come contesto di quella nuova. Puoi rendere questa relazione esplicita — e significativa — usando raise NewException from original:
def load_data(path):
try:
with open(path) as f:
return f.read()
except OSError as e:
raise RuntimeError("Failed to load configuration") from e
try:
load_data("config.json")
except RuntimeError as e:
print(f"Error: {e}")
print(f"Caused by: {e.__cause__}")
# Output:
# Error: Failed to load configuration
# Caused by: [Errno 2] No such file or directory: 'config.json'Quando Python stampa il traceback mostra entrambe le eccezioni in ordine, rendendo chiaro che il RuntimeError era una diretta conseguenza dell'OSError. Questo è particolarmente utile nel codice di libreria, dove vuoi tradurre errori OS di basso livello in errori di dominio di livello superiore senza nascondere la causa principale.
Sopprimere la Catena con raise ... from None
A volte l'eccezione originale è un dettaglio implementativo che non vuoi esporre. Passa None come causa per nasconderla:
def fetch(url):
try:
raise ConnectionError("timeout")
except ConnectionError:
raise RuntimeError("Network unavailable") from None
try:
fetch("http://example.com")
except RuntimeError as e:
print(f"Error: {e}")
print(f"Cause hidden: {e.__cause__}")
# Output:
# Error: Network unavailable
# Cause hidden: NoneIl traceback mostrerà solo il RuntimeError. Usa questa tecnica con parsimonia — nascondere la causa principale rende il debug più difficile per i consumatori della libreria.
Creare Classi di Eccezione Personalizzate
Le eccezioni built-in coprono gli errori di programmazione comuni, ma sono troppo generiche per i problemi di dominio. Se la tua applicazione di e-commerce solleva un semplice ValueError quando un pagamento fallisce, i chiamanti non possono distinguerlo da un argomento di funzione non valido. Le classi di eccezione personalizzate risolvono questo problema.
Un'eccezione personalizzata è semplicemente una classe che eredita da Exception (o da una delle sue sottoclassi):
class InsufficientFundsError(Exception):
"""Raised when a bank account has insufficient funds."""
def __init__(self, amount, balance):
self.amount = amount
self.balance = balance
super().__init__(
f"Cannot withdraw {amount}: balance is only {balance}"
)
class BankAccount:
def __init__(self, balance):
self.balance = balance
def withdraw(self, amount):
if amount > self.balance:
raise InsufficientFundsError(amount, self.balance)
self.balance -= amount
return self.balance
account = BankAccount(100)
try:
account.withdraw(150)
except InsufficientFundsError as e:
print(e)
print(f"You tried to withdraw: {e.amount}")
print(f"Available balance: {e.balance}")
# Output:
# Cannot withdraw 150: balance is only 100
# You tried to withdraw: 150
# Available balance: 100Punti chiave di questo schema:
super().__init__(message)imposta la stringa leggibile restituita dastr(e).- Gli attributi aggiuntivi (
self.amount,self.balance) consentono ai chiamanti di accedere a dati strutturati dall'eccezione, non solo a una stringa. - Una docstring chiara documenta quando l'eccezione deve essere sollevata.
Costruire una Gerarchia di Eccezioni
Le applicazioni reali hanno spesso molti tipi di errore correlati. Raggrupparli sotto una classe base condivisa consente ai chiamanti di intercettare sia l'errore specifico sia l'intera categoria:
class AppError(Exception):
"""Base class for all application errors."""
class ValidationError(AppError):
"""Raised when user input fails validation."""
class DatabaseError(AppError):
"""Raised when a database operation fails."""
def validate_username(name):
if len(name) < 3:
raise ValidationError(f"Username '{name}' is too short (min 3 chars)")
try:
validate_username("ab")
except ValidationError as e:
print(f"Validation failed: {e}")
except AppError as e:
print(f"Application error: {e}")
# Output:
# Validation failed: Username 'ab' is too short (min 3 chars)Un chiamante che vuole intercettare solo gli errori di database può scrivere except DatabaseError. Un chiamante che vuole intercettare qualsiasi problema dalla tua libreria può scrivere except AppError. Questo rispecchia il design della gerarchia di eccezioni di Python stessa, dove OSError raggruppa FileNotFoundError, PermissionError e molte altre.
Linee Guida per le Eccezioni Personalizzate
- Eredita da
Exception, non daBaseException.BaseExceptionè la radice della gerarchia di Python e include ancheSystemExiteKeyboardInterrupt, che non dovrebbero essere intercettati accidentalmente. - Termina il nome della classe in
Errorper le eccezioni che segnalano un problema. Questo segue le convenzioni di Python (ValueError,TypeError,IOError). - Mantieni la classe minimale a meno che tu non abbia bisogno di attributi aggiuntivi. Un corpo vuoto con una docstring è perfettamente valido.
- Inserisci le eccezioni in un modulo dedicato (es.
exceptions.py) per i progetti più grandi, in modo che i chiamanti possano importarle senza dover importare il resto del codice.
L'istruzione assert
assert è un modo leggero per esprimere invarianti — condizioni che devono essere vere affinché il codice sia corretto:
def divide(a, b):
assert b != 0, "Divisor must not be zero"
return a / b
try:
divide(10, 0)
except AssertionError as e:
print(f"AssertionError: {e}")
print(divide(10, 2))
# Output:
# AssertionError: Divisor must not be zero
# 5.0assert condition, message solleva AssertionError con il messaggio specificato quando condition è False.
Limitazione importante: Python rimuove le istruzioni assert quando viene eseguito con il flag -O (ottimizza). Questo significa:
- Usa
assertsolo per controlli di coerenza interna e ausili al debug. - Usa
raisecon un'eccezione appropriata per la validazione dell'input utente e i controlli API pubblici che devono essere sempre eseguiti.
Errori Comuni
Intercettare e ignorare silenziosamente le eccezioni
# Bad — the error disappears
try:
result = risky_operation()
except Exception:
pass
# Better — at minimum, log or re-raise
try:
result = risky_operation()
except Exception as e:
print(f"Operation failed: {e}")
raiseSollevare una stringa invece di un'eccezione
# Wrong — strings are not exceptions
raise "something went wrong" # TypeError
# Correct
raise ValueError("something went wrong")Intercettare BaseException accidentalmente
# Dangerous — this catches KeyboardInterrupt and SystemExit too
except BaseException:
...
# Use Exception instead
except Exception:
...Riepilogo
| Tecnica | Quando usarla |
|---|---|
raise ExceptionType("msg") | Segnalare un problema che il chiamante deve gestire |
raise (senza argomenti) | Risollevare l'eccezione corrente dopo la registrazione o la pulizia |
raise NewError(...) from original | Tradurre un errore di basso livello in uno di livello superiore, preservando la causa |
raise NewError(...) from None | Tradurre un errore nascondendo la causa interna |
| Classe di eccezione personalizzata | Assegnare agli errori specifici del dominio un tipo univoco e intercettabile |
| Gerarchia di eccezioni | Consentire ai chiamanti di intercettare categorie di errori ampie o specifiche |
assert | Verificare gli invarianti interni solo durante lo sviluppo |
Per il quadro completo dell'intercettazione e gestione delle eccezioni, vedi Python Try...Except. Per capire come le eccezioni personalizzate si inseriscono nel design delle classi, consulta Python Classes and Objects e Python Inheritance.