Un programma reale non può assumere che tutto vada sempre secondo i piani.
L'utente può inserire un valore non valido, un'operazione può non essere consentita, una risorsa può non essere disponibile oppure una funzione può ricevere dati che non può elaborare.
Python affronta queste situazioni attraverso le eccezioni.
In questo capitolo impareremo a:
try e except;else e finally;raise;with;Nota: il prossimo capitolo sarà dedicato a File I/O. Qui introdurremo soltanto il minimo necessario sui context manager per capire perché
withè importante nella gestione delle risorse.
Prima di parlare di eccezioni è utile distinguere due grandi categorie di problemi.
Sono errori che impediscono al programma di essere interpretato correttamente.
if True
print("Ciao")
Manca : dopo True.
Python segnala un:
SyntaxError
Un altro esempio:
print("Ciao"
Anche in questo caso la sintassi è incompleta.
Gli errori di sintassi devono essere corretti nel codice: non
vengono normalmente gestiti con try/except.
Un programma può invece essere sintatticamente corretto ma incontrare un problema durante l'esecuzione.
numero = int("abc")
Il codice è sintatticamente valido, ma la stringa "abc" non può essere
convertita in un intero.
Python genera:
ValueError
Un altro esempio:
risultato = 10 / 0
genera:
ZeroDivisionError
Questi sono esempi di eccezioni.
Problema Esempio Tipologia
sintassi non valida if True senza : SyntaxError
conversione non valida int("abc") ValueError
tipo non compatibile "10" + 5 TypeError
divisione per zero 10 / 0 ZeroDivisionError
indice inesistente lista[10] IndexError
chiave inesistente dizionario["x"] KeyError
La distinzione fondamentale è:
Un errore di sintassi riguarda la validità del codice; un'eccezione rappresenta un problema che si verifica durante l'esecuzione.
Consideriamo:
numero = int("abc")
print("Fine")
Quando viene eseguito:
int("abc")
Python genera un ValueError.
L'esecuzione della funzione corrente si interrompe e l'eccezione risale nello stack delle chiamate cercando un gestore compatibile.
Se nessuno la gestisce, il programma termina e Python mostra un traceback.
Il traceback contiene informazioni utili, tra cui:
Imparare a leggere un traceback è una competenza fondamentale per il debugging.
try/exceptLa struttura base è:
try:
istruzioni_rischiose
except TipoEccezione:
gestione_dell_errore
Esempio:
try:
numero = int(input("Inserisci un numero: "))
print(numero)
except ValueError:
print("Devi inserire un numero valido")
Se l'utente inserisce:
25
non si verifica alcuna eccezione.
Se inserisce:
ciao
viene eseguito il blocco except.
┌─────────────┐
│ try │
└──────┬──────┘
│
┌──────▼──────┐
│ eccezione? │
└──┬───────┬──┘
│ no │ sì
│ │
▼ ▼
continua except
Se il codice nel try termina senza eccezioni, il blocco except viene
saltato.
Evitiamo di scrivere:
try:
# cinquanta righe di codice
...
except ValueError:
...
È preferibile limitare il try all'operazione che può realmente
generare l'eccezione.
Meglio:
testo = input("Numero: ")
try:
numero = int(testo)
except ValueError:
print("Numero non valido")
else:
print(numero)
In questo modo è più facile capire quale operazione stiamo proteggendo.
Un
trytroppo grande può nascondere errori che non avevamo intenzione di gestire.
Possiamo avere più blocchi except:
try:
numero = int(input("Numero: "))
risultato = 10 / numero
except ValueError:
print("Devi inserire un numero")
except ZeroDivisionError:
print("Non puoi dividere per zero")
Python esegue il primo except compatibile con l'eccezione generata.
Se l'utente inserisce:
abc
si verifica:
ValueError
Se inserisce:
0
la conversione riesce, ma:
10 / 0
genera:
ZeroDivisionError
exceptSe vogliamo gestire nello stesso modo più eccezioni:
try:
...
except (ValueError, TypeError):
print("Dato non valido")
Le parentesi contengono una tupla di tipi di eccezione.
Questo è utile quando la strategia di gestione è identica.
Se invece i due errori richiedono messaggi o comportamenti diversi, è meglio utilizzare due blocchi distinti.
ValueErrorIl tipo è corretto, ma il valore non è accettabile.
int("abc")
TypeErrorL'operazione non è compatibile con il tipo degli oggetti.
"10" + 5
ZeroDivisionErrorDivisione intera, floating point o modulo per zero quando non consentiti.
10 / 0
IndexErrorIndice fuori dai limiti di una sequenza.
numeri = [10, 20, 30]
print(numeri[10])
KeyErrorAccesso a una chiave inesistente:
utente = {"nome": "Anna"}
print(utente["email"])
FileNotFoundErrorTentativo di aprire un file che non esiste.
open("inesistente.txt")
Il File I/O verrà approfondito nel prossimo capitolo.
AttributeErrorTentativo di utilizzare un attributo o metodo inesistente:
numero = 10
numero.saluta()
NameErrorUso di un nome che Python non conosce:
print(nome_inesistente)
ImportError e ModuleNotFoundErrorPossono verificarsi durante gli import quando un modulo o un elemento non è disponibile.
Le eccezioni sono organizzate in una gerarchia di classi.
In alto troviamo:
BaseException
├── BaseExceptionGroup
├── GeneratorExit
├── KeyboardInterrupt
├── SystemExit
└── Exception
La maggior parte delle eccezioni applicative deriva da:
Exception
Per esempio, concettualmente:
Exception
├── ValueError
├── TypeError
├── LookupError
│ ├── IndexError
│ └── KeyError
└── OSError
└── FileNotFoundError
Questo spiega perché possiamo scrivere:
except LookupError:
...
per intercettare sia IndexError sia KeyError.
Exception vs BaseExceptionIn generale, nelle applicazioni si gestiscono eccezioni derivate da:
Exception
Non è buona pratica scrivere:
except BaseException:
...
perché questo include anche segnali come KeyboardInterrupt e
SystemExit, che normalmente devono poter interrompere il programma.
exceptL'ordine dei blocchi è importante.
Sbagliato:
try:
...
except Exception:
print("Errore generico")
except ValueError:
print("Valore non valido")
Il secondo blocco è di fatto irraggiungibile per un ValueError, perché
ValueError è già una sottoclasse di Exception.
Meglio:
try:
...
except ValueError:
print("Valore non valido")
except Exception:
print("Altro errore")
Regola:
Prima le eccezioni più specifiche, poi quelle più generiche.
Possiamo ottenere l'oggetto eccezione con as:
try:
numero = int("abc")
except ValueError as errore:
print("Errore:", errore)
L'oggetto errore contiene informazioni sull'eccezione.
Possiamo utilizzarlo per produrre un messaggio più preciso:
try:
numero = int(testo)
except ValueError as errore:
print(f"Conversione fallita: {errore}")
In un'applicazione reale, il messaggio dell'eccezione può essere utile al programmatore ma non necessariamente adatto all'utente finale.
Possiamo quindi separare:
dettaglio tecnico → log
messaggio comprensibile → utente
elseIl blocco else viene eseguito solo se nel try non si è verificata
alcuna eccezione.
try:
numero = int(input("Numero: "))
except ValueError:
print("Input non valido")
else:
print(f"Hai inserito {numero}")
Permette di separare:
Esempio:
try:
numero = int(input("Numero: "))
except ValueError:
print("Numero non valido")
else:
risultato = numero * 2
print(risultato)
È spesso più chiaro di inserire tutto nel try.
finallyIl blocco finally viene eseguito comunque, indipendentemente dal
fatto che si verifichi o meno un'eccezione.
try:
numero = int(input("Numero: "))
except ValueError:
print("Errore")
finally:
print("Operazione terminata")
Il finally viene eseguito:
È particolarmente utile per la pulizia delle risorse:
Nel prossimo capitolo vedremo come with renda molte di queste
operazioni più sicure e leggibili.
try / except / else / finallyLa struttura completa è:
try:
operazione
except TipoErrore:
gestione_errore
else:
operazione_se_tutto_ok
finally:
pulizia
Esempio:
try:
numero = int(input("Numero: "))
risultato = 100 / numero
except ValueError:
print("Devi inserire un numero")
except ZeroDivisionError:
print("Non puoi dividere per zero")
else:
print("Risultato:", risultato)
finally:
print("Elaborazione terminata")
try
│
├── errore ──► except
│
└── nessun errore ──► else
│
▼
finally
Il finally viene eseguito alla fine del costrutto.
raise: generare un'eccezioneNon dobbiamo soltanto gestire eccezioni generate da Python.
Possiamo generarne una noi:
eta = -5
if eta < 0:
raise ValueError("L'età non può essere negativa")
Questo è molto importante nella progettazione delle funzioni e delle classi.
def dividi(a, b):
if b == 0:
raise ValueError("Il divisore non può essere zero")
return a / b
Poi:
try:
print(dividi(10, 0))
except ValueError as errore:
print(errore)
La funzione segnala che i propri dati non rispettano una regola; il codice chiamante decide come gestire la situazione.
raiseLe eccezioni sono molto utili per imporre invarianti.
class ContoBancario:
def __init__(self, saldo):
if saldo < 0:
raise ValueError("Il saldo iniziale non può essere negativo")
self.saldo = saldo
Ora:
conto = ContoBancario(100)
è valido.
Mentre:
conto = ContoBancario(-50)
genera un'eccezione.
Questo si collega direttamente al capitolo precedente:
una classe può utilizzare le eccezioni per proteggere lo stato dell'oggetto.
Possiamo usare raise anche in una property:
class Prodotto:
def __init__(self, prezzo):
self.prezzo = prezzo
@property
def prezzo(self):
return self._prezzo
@prezzo.setter
def prezzo(self, valore):
if valore < 0:
raise ValueError("Il prezzo non può essere negativo")
self._prezzo = valore
Uso:
prodotto = Prodotto(20)
prodotto.prezzo = 25
ma:
prodotto.prezzo = -10
genera:
ValueError
La classe mantiene così una regola interna coerente.
A volte vogliamo intercettare un'eccezione solo per eseguire una certa operazione, ma non vogliamo nasconderla.
Possiamo usare:
raise
senza specificare un nuovo tipo.
Esempio:
try:
numero = int("abc")
except ValueError:
print("Errore durante la conversione")
raise
Il messaggio viene stampato, ma l'eccezione continua a risalire.
Per esempio per:
Non usare
raiseper nascondere l'errore: serve a permettere all'eccezione di proseguire la propria propagazione.
Possiamo definire nuove eccezioni creando una classe derivata da
Exception.
class SaldoInsufficienteError(Exception):
pass
Ora:
def preleva(saldo, importo):
if importo > saldo:
raise SaldoInsufficienteError(
"Saldo insufficiente"
)
return saldo - importo
Gestione:
try:
saldo = preleva(100, 150)
except SaldoInsufficienteError as errore:
print(errore)
Un tipo specifico comunica meglio il significato dell'errore.
Con:
ValueError
sappiamo che c'è un problema con un valore.
Con:
SaldoInsufficienteError
sappiamo immediatamente quale condizione applicativa si è verificata.
Possiamo costruire una gerarchia.
class BibliotecaError(Exception):
pass
class LibroNonTrovatoError(BibliotecaError):
pass
class LibroNonDisponibileError(BibliotecaError):
pass
Ora possiamo gestire un errore specifico:
try:
biblioteca.presta("123")
except LibroNonDisponibileError:
print("Il libro è già in prestito")
oppure tutti gli errori della biblioteca:
try:
...
except BibliotecaError:
print("Si è verificato un errore nella biblioteca")
Questo è un ottimo strumento per progettare API e applicazioni complesse.
Supponiamo di avere:
def cerca_utente(id_utente):
...
Possiamo decidere che un utente inesistente generi:
class UtenteNonTrovatoError(Exception):
pass
Il chiamante può quindi scrivere:
try:
utente = cerca_utente(123)
except UtenteNonTrovatoError:
print("Utente non trovato")
La funzione non decide cosa visualizzare.
Decide soltanto:
questa situazione rappresenta un errore di dominio.
La scelta di cosa fare spetta al livello superiore.
Questo principio è importante per separare logica applicativa e presentazione.
Possiamo trasformare un'eccezione tecnica in un'eccezione più significativa utilizzando:
raise ... from ...
Esempio:
class ConfigurazioneError(Exception):
pass
def leggi_configurazione():
try:
valore = int("abc")
except ValueError as errore:
raise ConfigurazioneError(
"Configurazione non valida"
) from errore
Ora abbiamo due informazioni:
Il traceback mostrerà la relazione tra le due eccezioni.
Questo è particolarmente utile quando un livello dell'applicazione vuole esporre un errore coerente con il proprio dominio senza perdere la causa tecnica originale.
raise ... from NoneIn alcuni casi possiamo decidere di non mostrare il chaining della causa:
try:
int("abc")
except ValueError:
raise ConfigurazioneError(
"Configurazione non valida"
) from None
La causa continua a essere disponibile nel contesto dell'eccezione, ma Python non mostrerà il chaining nello stesso modo nel traceback.
Questa tecnica va usata con criterio: nascondere informazioni diagnostiche può rendere più difficile il debugging.
except: genericoÈ possibile scrivere:
try:
...
except:
...
ma generalmente è una cattiva pratica.
Questo cattura anche eccezioni che normalmente non dovremmo
intercettare, come KeyboardInterrupt e SystemExit.
Meglio:
except ValueError:
...
oppure, quando serve davvero un fallback generale:
except Exception as errore:
...
Perché questo codice:
try:
...
except Exception:
print("Qualcosa è andato storto")
può nascondere problemi completamente diversi tra loro.
Un errore di programmazione non dovrebbe diventare automaticamente un generico "dato non valido".
Le eccezioni servono a gestire condizioni eccezionali, non a sostituire
normalmente if.
Da evitare:
try:
elemento = lista[indice]
except IndexError:
elemento = None
se il controllo dell'indice fa parte del normale flusso del programma e può essere verificato chiaramente prima.
Meglio, quando appropriato:
if 0 <= indice < len(lista):
elemento = lista[indice]
else:
elemento = None
Non è una regola assoluta: in Python è perfettamente legittimo usare un'eccezione quando rappresenta naturalmente una condizione che si verifica durante un'operazione. L'importante è non trasformare ogni normale decisione in un meccanismo basato su eccezioni.
Python viene spesso descritto attraverso due approcci.
"Guarda prima di saltare."
if indice < len(lista):
valore = lista[indice]
"È più facile chiedere perdono che permesso."
try:
valore = lista[indice]
except IndexError:
valore = None
Entrambi gli approcci possono essere validi.
La scelta dipende dal contesto.
In Python è spesso naturale usare:
try:
valore = dizionario["nome"]
except KeyError:
valore = "Sconosciuto"
oppure:
valore = dizionario.get("nome", "Sconosciuto")
Nel secondo caso non serve un'eccezione perché il comportamento desiderato è già fornito dall'API del dizionario.
Le eccezioni non sono automaticamente migliori dei controlli condizionali: bisogna scegliere lo strumento più espressivo.
finally e gestione delle risorseUn esempio classico:
f = None
try:
f = open("dati.txt", "r")
contenuto = f.read()
except FileNotFoundError:
print("File non trovato")
finally:
if f is not None:
f.close()
Il finally garantisce che il tentativo di chiusura venga eseguito
anche se si verifica un errore.
Tuttavia, Python offre una soluzione migliore per molte risorse:
with open("dati.txt", "r") as f:
contenuto = f.read()
Il blocco with gestisce automaticamente l'uscita dalla risorsa.
Il File I/O e l'uso approfondito di with open(...) saranno
argomento del prossimo capitolo.
Un context manager definisce cosa deve accadere entrando e uscendo da un contesto.
La forma:
with risorsa:
...
è utilizzata per garantire una corretta gestione delle risorse.
Esempi tipici:
Un context manager può essere implementato attraverso:
__enter__()
__exit__()
Esempio semplificato:
class Risorsa:
def __enter__(self):
print("Apertura risorsa")
return self
def __exit__(self, exc_type, exc_value, traceback):
print("Chiusura risorsa")
Uso:
with Risorsa():
print("Uso la risorsa")
Output:
Apertura risorsa
Uso la risorsa
Chiusura risorsa
Questo meccanismo sarà fondamentale quando lavoreremo con i file.
Il metodo __exit__() riceve informazioni sull'eventuale eccezione.
class Gestore:
def __enter__(self):
print("Entrata")
return self
def __exit__(self, exc_type, exc_value, traceback):
print("Uscita")
if exc_type is not None:
print("Si è verificata un'eccezione")
Se il blocco genera un errore:
with Gestore():
raise ValueError("Problema")
__exit__() viene comunque chiamato.
Il valore restituito da __exit__() può anche determinare se
l'eccezione viene soppressa. Nella pratica, non bisogna sopprimere
eccezioni senza una precisa ragione.
contextlibPython fornisce il modulo contextlib per creare context manager più
facilmente.
Un esempio:
from contextlib import contextmanager
@contextmanager
def risorsa():
print("Apertura")
try:
yield
finally:
print("Chiusura")
Uso:
with risorsa():
print("Sto lavorando")
Il finally garantisce la pulizia anche se nel corpo del with si
verifica un'eccezione.
Non è necessario utilizzare sempre contextlib: per molte risorse
Python fornisce già context manager pronti all'uso.
Una funzione può:
Esempio:
def converti_intero(testo):
return int(testo)
La funzione non deve necessariamente contenere:
try:
...
except:
...
Se testo non è convertibile, può lasciare che il ValueError venga
propagato.
Il chiamante decide:
try:
numero = converti_intero("abc")
except ValueError:
print("Input non valido")
Questo spesso produce un design più pulito.
Una buona regola è:
Gestisci l'eccezione nel punto in cui sai cosa farne.
Esempio:
def calcola_totale(prezzo, quantita):
return prezzo * quantita
Se un valore è negativo e questo rappresenta una violazione del contratto della funzione, possiamo generare:
def calcola_totale(prezzo, quantita):
if prezzo < 0:
raise ValueError("Prezzo negativo")
if quantita < 0:
raise ValueError("Quantità negativa")
return prezzo * quantita
Il livello superiore decide invece se:
Questa separazione rende il codice più riutilizzabile.
Vogliamo chiedere all'utente un numero intero.
def leggi_intero():
while True:
testo = input("Inserisci un intero: ")
try:
return int(testo)
except ValueError:
print("Valore non valido. Riprova.")
Uso:
numero = leggi_intero()
print("Hai inserito:", numero)
Qui l'eccezione rappresenta una condizione prevista dell'interazione con l'utente.
La funzione continua a chiedere il valore finché non ne riceve uno valido.
def leggi_numero():
while True:
try:
return float(input("Numero: "))
except ValueError:
print("Inserisci un numero valido")
def dividi(a, b):
if b == 0:
raise ZeroDivisionError("Il divisore non può essere zero")
return a / b
Programma:
a = leggi_numero()
b = leggi_numero()
try:
risultato = dividi(a, b)
except ZeroDivisionError as errore:
print(errore)
else:
print("Risultato:", risultato)
Abbiamo separato:
Riprendiamo il concetto del capitolo precedente.
class SaldoInsufficienteError(Exception):
pass
class ContoBancario:
def __init__(self, intestatario, saldo=0):
if saldo < 0:
raise ValueError("Saldo iniziale non valido")
self.intestatario = intestatario
self.saldo = saldo
def deposita(self, importo):
if importo <= 0:
raise ValueError("L'importo deve essere positivo")
self.saldo += importo
def preleva(self, importo):
if importo <= 0:
raise ValueError("L'importo deve essere positivo")
if importo > self.saldo:
raise SaldoInsufficienteError(
"Saldo insufficiente"
)
self.saldo -= importo
Utilizzo:
conto = ContoBancario("Anna", 100)
try:
conto.preleva(150)
except SaldoInsufficienteError:
print("Operazione non consentita")
La classe definisce le regole del dominio.
Il codice chiamante decide come reagire.
In un'applicazione più grande possiamo avere:
Interfaccia
│
▼
Servizio
│
▼
Repository
│
▼
Database
Un livello basso può generare un'eccezione:
DatabaseError
Il livello di servizio può trasformarla in:
OrdineNonDisponibileError
e l'interfaccia può trasformarla in un messaggio comprensibile:
Non è stato possibile completare l'ordine.
La gestione delle eccezioni diventa quindi uno strumento per separare i livelli dell'applicazione.
È utile distinguere:
FileNotFoundError
ConnectionError
TimeoutError
Descrive un problema legato all'infrastruttura.
SaldoInsufficienteError
LibroNonDisponibileError
OrdineNonValidoError
Descrive una regola dell'applicazione.
Una buona architettura evita di far arrivare necessariamente gli errori tecnici fino all'interfaccia utente.
printDurante lo sviluppo possiamo usare:
print("Errore")
ma nelle applicazioni reali è spesso preferibile il modulo logging.
Esempio:
import logging
logging.basicConfig(level=logging.INFO)
logging.info("Avvio applicazione")
logging.warning("Dato sospetto")
logging.error("Operazione fallita")
Per un'eccezione:
try:
numero = int("abc")
except ValueError:
logging.exception("Conversione fallita")
logging.exception() è particolarmente utile all'interno di un except
perché registra anche le informazioni dell'eccezione.
Il logging verrà approfondito quando affronteremo applicazioni più strutturate.
Questo codice è pericoloso:
try:
esegui_operazione()
except Exception:
pass
Se l'operazione fallisce, non succede nulla.
Il problema può rimanere completamente invisibile.
Meglio:
try:
esegui_operazione()
except Exception as errore:
logging.exception("Operazione fallita")
oppure, se non sappiamo gestire l'errore:
try:
esegui_operazione()
except Exception:
raise
Il principio è:
Un'eccezione deve essere gestita, trasformata o propagata; raramente deve essere semplicemente ignorata.
Non tutte le eccezioni devono essere catturate.
Se scriviamo:
def calcola_media(valori):
return sum(valori) / len(valori)
e il programma contiene un bug che passa un oggetto completamente sbagliato, catturare ogni possibile eccezione potrebbe nascondere il problema.
Meglio lasciare emergere gli errori di programmazione durante lo sviluppo.
Le eccezioni vanno gestite soprattutto quando:
except Exception ovunqueDa evitare:
def funzione1():
try:
...
except Exception:
...
def funzione2():
try:
funzione1()
except Exception:
...
def funzione3():
try:
funzione2()
except Exception:
...
Questo rende praticamente impossibile capire dove sia il problema.
È preferibile stabilire un punto chiaro in cui l'errore viene:
assert non sostituisce raisePython offre anche:
assert condizione
Esempio:
assert eta >= 0
Se la condizione è falsa viene generato:
AssertionError
Le asserzioni sono utili soprattutto per controllare invarianti e condizioni che il programmatore considera necessariamente vere.
Non dovrebbero essere usate come unico sistema di validazione degli input dell'utente.
Per esempio, per validare un dato esterno è preferibile:
if eta < 0:
raise ValueError("Età non valida")
piuttosto che:
assert eta >= 0
Possiamo documentare chiaramente quali dati riceve una funzione:
def dividi(a: float, b: float) -> float:
if b == 0:
raise ZeroDivisionError("Divisore nullo")
return a / b
I type hints non fanno però comparire automaticamente un controllo a runtime.
Il tipo:
float
non significa che Python impedirà in ogni caso di passare un altro tipo.
Le eccezioni rimangono quindi importanti anche in presenza dei type hints.
try?except?else?finally?raise?except è importante?raise ... from ...?except:?Exception e BaseException?with?except Exception: pass?try termina senza eccezioni.Exception.except vengono verificati dall'alto verso il basso.Exception è la base della maggior parte degli errori applicativi;
BaseException è più generale e comprende anche SystemExit e
KeyboardInterrupt.Scrivi una funzione:
leggi_intero()
che continui a chiedere all'utente un numero finché non viene inserito un intero valido.
Utilizza try/except ValueError.
Scrivi:
dividi(a, b)
che:
a / b;Crea una classe:
Prodotto
con:
Il prezzo non può essere negativo.
Utilizza una property e raise ValueError.
Crea:
class SaldoInsufficienteError(Exception):
pass
e utilizzala in una classe ContoBancario.
Il metodo preleva() deve sollevare l'eccezione se l'importo supera il
saldo.
Riprendi la biblioteca della sfida del capitolo 5.
Definisci:
class BibliotecaError(Exception):
pass
class LibroNonTrovatoError(BibliotecaError):
pass
class LibroNonDisponibileError(BibliotecaError):
pass
Utilizza queste eccezioni nei metodi:
cerca()
presta()
restituisci()
rimuovi()
Completa il sistema di gestione ordini introdotto nei capitoli precedenti.
Definisci le eccezioni:
class OrdineError(Exception):
pass
class ProdottoNonDisponibileError(OrdineError):
pass
class QuantitaNonValidaError(OrdineError):
pass
class OrdineVuotoError(OrdineError):
pass
ProdottoDeve contenere:
La quantità non può essere negativa.
OrdineDeve permettere di:
aggiungi(prodotto, quantita)
Se la quantità è minore o uguale a zero:
raise QuantitaNonValidaError(...)
Se non c'è disponibilità:
raise ProdottoNonDisponibileError(...)
Definisci:
@property
def totale(self):
...
che calcoli il totale dell'ordine.
Definisci:
conferma()
che non permetta di confermare un ordine vuoto.
In caso contrario:
raise OrdineVuotoError(...)
Gestisci separatamente le diverse condizioni:
try:
...
except QuantitaNonValidaError:
...
except ProdottoNonDisponibileError:
...
except OrdineVuotoError:
...
Aggiungi infine:
except OrdineError:
...
come fallback per gli errori specifici dell'applicazione.
Una possibile implementazione:
class OrdineError(Exception):
pass
class ProdottoNonDisponibileError(OrdineError):
pass
class QuantitaNonValidaError(OrdineError):
pass
class OrdineVuotoError(OrdineError):
pass
class Prodotto:
def __init__(self, codice, nome, prezzo, disponibilita):
if prezzo < 0:
raise ValueError("Prezzo non valido")
if disponibilita < 0:
raise ValueError("Disponibilità non valida")
self.codice = codice
self.nome = nome
self.prezzo = prezzo
self.disponibilita = disponibilita
class Ordine:
def __init__(self):
self._righe = []
def aggiungi(self, prodotto, quantita):
if quantita <= 0:
raise QuantitaNonValidaError(
"La quantità deve essere positiva"
)
if quantita > prodotto.disponibilita:
raise ProdottoNonDisponibileError(
f"Disponibilità insufficiente per {prodotto.nome}"
)
self._righe.append((prodotto, quantita))
prodotto.disponibilita -= quantita
@property
def totale(self):
return sum(
prodotto.prezzo * quantita
for prodotto, quantita in self._righe
)
def conferma(self):
if not self._righe:
raise OrdineVuotoError(
"Non puoi confermare un ordine vuoto"
)
print(
f"Ordine confermato: {self.totale:.2f} €"
)
Programma:
mouse = Prodotto(
"M01",
"Mouse",
25.90,
10
)
ordine = Ordine()
try:
ordine.aggiungi(mouse, 3)
ordine.conferma()
except QuantitaNonValidaError as errore:
print("Quantità:", errore)
except ProdottoNonDisponibileError as errore:
print("Disponibilità:", errore)
except OrdineVuotoError as errore:
print("Ordine:", errore)
except OrdineError as errore:
print("Errore ordine:", errore)
Nella soluzione precedente la disponibilità viene decrementata quando il prodotto viene aggiunto all'ordine.
In un sistema reale potremmo voler distinguere:
disponibilità
│
├── quantità disponibile
├── quantità riservata
└── quantità acquistata
La gestione corretta dipende dal dominio applicativo.
Questo esempio serve soprattutto a esercitare la progettazione delle eccezioni.
Consideriamo:
def presta_libro(isbn):
...
Se il libro non è disponibile, la funzione può generare:
LibroNonDisponibileError
Non dovrebbe necessariamente fare:
print("ERRORE!!!")
perché non sa se il chiamante sia:
La funzione deve comunicare il problema.
Il chiamante decide come presentarlo.
Questa separazione diventa fondamentale nello sviluppo di applicazioni professionali.
Possiamo riassumere il capitolo in alcune regole pratiche.
except ValueError:
...
è generalmente meglio di:
except:
...
Da evitare:
except Exception:
pass
Meglio:
raise ValueError("Il prezzo non può essere negativo")
che:
raise ValueError("Errore")
SaldoInsufficienteError
LibroNonDisponibileError
OrdineVuotoError
comunicano molto più di un generico Exception.
Non è obbligatorio mettere un try/except in ogni funzione.
finally o context manager per le risorseLa pulizia delle risorse deve essere garantita anche in caso di errore.
Se un'API offre già un metodo come:
dict.get()
può essere preferibile usarlo invece di generare e catturare un
KeyError.
Le eccezioni permettono a Python di rappresentare e gestire condizioni anomale durante l'esecuzione.
La struttura fondamentale è:
try:
...
except TipoErrore:
...
Possiamo aggiungere:
else:
...
per il caso di successo e:
finally:
...
per le operazioni che devono essere eseguite comunque.
Possiamo generare eccezioni:
raise ValueError("...")
e creare eccezioni personalizzate:
class MioErrore(Exception):
pass
Possiamo inoltre trasformare e collegare le eccezioni:
raise MioErrore("...") from errore_originale
Abbiamo visto che:
except dovrebbe essere specifico;Exception è la base usuale degli errori applicativi;BaseException non va catturata genericamente;else rappresenta il percorso di successo;finally garantisce la fase finale di pulizia;raise permette di comunicare una violazione di una regola;with e i context manager aiutano a gestire correttamente le
risorse.Il concetto più importante è forse questo:
Un'eccezione non è semplicemente qualcosa da "nascondere": è un messaggio strutturato che comunica che il normale percorso dell'esecuzione non può proseguire come previsto.
Ora abbiamo gli strumenti necessari per affrontare uno dei compiti più comuni nei programmi reali: leggere e scrivere dati su file.
Nel prossimo capitolo vedremo:
open();r, w, a, x;with;read();readline();readlines();write();writelines();pathlib;Il collegamento con questo capitolo sarà diretto.
Per esempio:
try:
with open("dati.txt", "r", encoding="utf-8") as file:
contenuto = file.read()
except FileNotFoundError:
print("Il file non esiste")
Qui convivono già due concetti:
File I/O
+
Gestione delle eccezioni
Ed è proprio questa combinazione che permette di costruire programmi capaci di lavorare con il mondo reale.