Introduzione
Quando si inizia a lavorare con le classi in Python, uno degli errori più comuni è esporre direttamente gli attributi pubblici e modificarli liberamente da ogni parte del programma. Questo approccio funziona nei casi semplici, ma diventa rapidamente fragile: i dati possono finire in uno stato incoerente, i controlli vengono duplicati e il codice diventa difficile da mantenere.
Un sotto-argomento molto utile e pratico delle classi Python è l’incapsulamento tramite proprietà. Le proprietà permettono di controllare l’accesso agli attributi, validare i valori in ingresso e mantenere l’oggetto sempre coerente, senza rinunciare a una sintassi pulita e naturale.
In questo tutorial vedremo come progettare una classe con attributi “protetti” da proprietà, usando un esempio realistico: una classe ContoBancario. L’obiettivo è impedire operazioni non valide, come impostare un saldo negativo o modificare il titolare con valori vuoti.
Codice completo
class ContoBancario:
def __init__(self, titolare: str, saldo_iniziale: float = 0.0):
self.titolare = titolare
self.saldo = saldo_iniziale # passa dal setter, quindi viene validato
@property
def titolare(self):
return self._titolare
@titolare.setter
def titolare(self, valore):
if not isinstance(valore, str):
raise TypeError("Il titolare deve essere una stringa.")
valore = valore.strip()
if not valore:
raise ValueError("Il titolare non può essere vuoto.")
self._titolare = valore
@property
def saldo(self):
return self._saldo
@saldo.setter
def saldo(self, valore):
if not isinstance(valore, (int, float)):
raise TypeError("Il saldo deve essere un numero.")
if valore < 0:
raise ValueError("Il saldo non può essere negativo.")
self._saldo = float(valore)
def deposita(self, importo: float):
if not isinstance(importo, (int, float)):
raise TypeError("L´importo deve essere numerico.")
if importo <= 0:
raise ValueError("L´importo da depositare deve essere positivo.")
self._saldo += float(importo)
def preleva(self, importo: float):
if not isinstance(importo, (int, float)):
raise TypeError("L´importo deve essere numerico.")
if importo <= 0:
raise ValueError("L´importo da prelevare deve essere positivo.")
if importo > self._saldo:
raise ValueError("Fondi insufficienti.")
self._saldo -= float(importo)
def riepilogo(self):
return f"Conto intestato a {self.titolare}: saldo attuale {self.saldo:.2f} €"
# Esempio di utilizzo
conto = ContoBancario(" Mario Rossi ", 150.0)
print(conto.riepilogo()) # Conto intestato a Mario Rossi: saldo attuale 150.00 €
conto.deposita(50)
print(conto.riepilogo()) # Conto intestato a Mario Rossi: saldo attuale 200.00 €
conto.preleva(30)
print(conto.riepilogo()) # Conto intestato a Mario Rossi: saldo attuale 170.00 €
# Accesso controllato tramite proprietà
conto.titolare = "Luigi Bianchi"
conto.saldo = 300
print(conto.riepilogo()) # Conto intestato a Luigi Bianchi: saldo attuale 300.00 € Spiegazione
La classe ContoBancario mostra un uso molto comune e importante delle classi in Python: proteggere lo stato interno con proprietà e metodi dedicati.
1. Attributi interni e naming convenzionale
Noterai che gli attributi reali sono chiamati _titolare e _saldo. Il prefisso underscore indica che questi valori sono pensati per uso interno alla classe. Non è un blocco assoluto, ma una convenzione forte: chi usa la classe dovrebbe evitare di modificarli direttamente.
2. Il decoratore @property
Il decoratore @property trasforma un metodo in un attributo “lettura”. Questo significa che puoi scrivere conto.saldo invece di conto.get_saldo(). Il vantaggio è che l’interfaccia resta semplice, ma dietro le quinte puoi aggiungere logica, controlli o calcoli.
3. Il setter della proprietà
Con @saldo.setter e @titolare.setter controlliamo ogni assegnazione. Se qualcuno prova a fare conto.saldo = -10, il programma solleva una ValueError. Questo è fondamentale per mantenere l’oggetto in uno stato valido.
4. Validazione centralizzata
Un grande vantaggio delle proprietà è che la validazione avviene in un solo punto. Se in futuro il criterio cambia, ad esempio vuoi permettere solo titolari con almeno due parole, modifichi il setter una sola volta e tutta la classe segue la nuova regola.
5. Metodi di dominio
I metodi deposita() e preleva() rappresentano le azioni consentite sul conto. Invece di modificare il saldo direttamente, si usano operazioni esplicite che rispettano le regole del dominio. Questo rende il codice più leggibile e meno soggetto a errori.
6. Perché non usare solo attributi pubblici?
Con attributi pubblici come conto.saldo = ... senza controllo, chiunque potrebbe impostare valori negativi o non numerici. In un progetto piccolo il problema può sembrare marginale, ma in un’applicazione reale i bug generati da stati incoerenti sono difficili da tracciare.
Best practice
- Usa proprietà quando serve controllo: se un attributo richiede validazione, trasformazione o calcolo, una property è spesso la scelta giusta.
- Conserva l’interfaccia semplice: l’utente della classe dovrebbe leggere e scrivere gli attributi in modo naturale, senza sapere troppo dei dettagli interni.
- Valida sempre nei setter: non affidarti al fatto che “chi usa la classe farà la cosa giusta”. Il controllo va fatto dentro la classe.
- Usa metodi per le operazioni di business: depositare, prelevare, ordinare, registrare, calcolare: queste azioni meritano metodi dedicati.
- Evita la duplicazione delle regole: se una regola è importante, non riscriverla in più punti del codice.
- Preferisci eccezioni chiare: TypeError per tipi errati, ValueError per valori non validi. Aiuta molto nel debug.
- Non abusare delle property: se un attributo non richiede logica, lasciarlo semplice è spesso la soluzione migliore.
Riepilogo
Le proprietà sono uno strumento essenziale per scrivere classi Python robuste e facili da usare. Ti permettono di mantenere l’oggetto in uno stato coerente, nascondere i dettagli interni e offrire un’interfaccia pulita a chi utilizza la classe.
Nel nostro esempio, la classe ContoBancario impedisce saldi negativi, valida il nome del titolare e centralizza la logica di controllo. Questo approccio rende il codice più affidabile, più leggibile e più facile da evolvere nel tempo.
Se stai progettando classi per applicazioni reali, imparare a usare bene @property e i setter è un passo fondamentale verso codice più professionale.
Approfondisci con risorse ufficiali
- Documentazione ufficiale Python sulle classi: https://docs.python.org/3/tutorial/classes.html
- Documentazione ufficiale Python su property(): https://docs.python.org/3/library/functions.html#property
- Data model di Python: https://docs.python.org/3/reference/datamodel.html
- PEP 8 - Style Guide for Python Code: https://peps.python.org/pep-0008/
