Classi e oggetti in Python: gestire l’incapsulamento dei dati con proprietà

by theArchitect
SHARE
Classi e oggetti in Python: gestire l’incapsulamento dei dati con proprietà
© Guida-HTML5.it

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

SHARE