Classi e oggetti in Python per modellare uno stato coerente con le proprietà

by theArchitect
SHARE
Classi e oggetti in Python per modellare uno stato coerente con le proprietà
© Guida-HTML5.it

Introduzione

Quando si parla di classi e oggetti in Python, spesso si pensa subito a strutture complesse, ereditarietà o metodi speciali. In realtà, uno degli usi più pratici e utili delle classi è la gestione di stato coerente: cioè creare oggetti che mantengono dati validi e consistenti nel tempo, evitando valori impossibili o incoerenti.

Questo è molto importante in applicazioni reali. Per esempio, un oggetto che rappresenta un conto bancario non dovrebbe mai avere un saldo negativo se la regola aziendale lo vieta. Un prodotto in magazzino non dovrebbe avere quantità inferiori a zero. Un ordine non dovrebbe risultare “spedito” se non è stato prima “pagato”.

Per ottenere questo risultato, in Python possiamo usare una classe con proprietà e incapsulamento. In pratica, non esponiamo direttamente gli attributi interni, ma controlliamo come vengono letti e modificati. Questo approccio è utile perché permette di validare i dati, proteggere lo stato dell’oggetto e rendere il codice più robusto e facile da mantenere.

In questo tutorial costruiremo una classe ContoBancario che gestisce il saldo in modo sicuro, impedendo modifiche non valide e offrendo metodi chiari per depositare e prelevare denaro.

Codice completo

class ContoBancario:
    def __init__(self, intestatario: str, saldo_iniziale: float = 0.0):
        self.intestatario = intestatario
        self._saldo = 0.0  # attributo "protetto" interno
        self.saldo = saldo_iniziale  # passa dal setter con validazione

    @property
    def saldo(self) -> float:
        """Restituisce il saldo attuale."""
        return self._saldo

    @saldo.setter
    def saldo(self, valore: float) -> None:
        """Imposta il saldo solo se il valore è valido."""
        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) -> None:
        """Aggiunge denaro al conto."""
        if not isinstance(importo, (int, float)):
            raise TypeError("L´importo deve essere un numero.")
        if importo <= 0:
            raise ValueError("L´importo da depositare deve essere positivo.")
        self._saldo += float(importo)

    def preleva(self, importo: float) -> None:
        """Rimuove denaro dal conto se il saldo è sufficiente."""
        if not isinstance(importo, (int, float)):
            raise TypeError("L´importo deve essere un numero.")
        if importo <= 0:
            raise ValueError("L´importo da prelevare deve essere positivo.")
        if importo > self._saldo:
            raise ValueError("Saldo insufficiente.")
        self._saldo -= float(importo)

    def trasferisci(self, altro_conto: "ContoBancario", importo: float) -> None:
        """Trasferisce denaro verso un altro conto."""
        if not isinstance(altro_conto, ContoBancario):
            raise TypeError("Il destinatario deve essere un ContoBancario.")
        self.preleva(importo)
        altro_conto.deposita(importo)

    def __str__(self) -> str:
        return f"Conto di {self.intestatario}: saldo {self._saldo:.2f}€"


# Esempio d´uso
conto1 = ContoBancario("Alice", 100.0)
conto2 = ContoBancario("Bob", 50.0)

print(conto1)
print(conto2)

conto1.deposita(25)
conto1.preleva(30)
conto1.trasferisci(conto2, 20)

print(conto1)
print(conto2)

# Accesso controllato alla proprietà
print("Saldo conto1:", conto1.saldo)

# Esempio di errore:
# conto1.saldo = -10

Spiegazione

La classe ContoBancario mostra un pattern molto comune in Python: usare un attributo interno, in questo caso _saldo, e controllarne l’accesso tramite una property.

1. Attributi pubblici e attributi “protetti”

In Python non esiste una vera protezione rigida come in altri linguaggi, ma per convenzione un attributo che inizia con _ indica che è destinato all’uso interno della classe. Non è vietato accedervi dall’esterno, ma è sconsigliato.

Nel nostro esempio, _saldo è l’attributo interno. L’utente della classe dovrebbe usare saldo, che passa dal getter e dal setter.

2. Il decoratore @property

Il decoratore @property trasforma un metodo in una proprietà leggibile come un attributo normale. Questo è molto utile perché permette di scrivere codice pulito senza rinunciare al controllo.

Quando scriviamo conto1.saldo, Python esegue automaticamente il metodo associato al getter e restituisce il valore di _saldo.

3. Il setter con validazione

Il metodo decorato con @saldo.setter viene chiamato quando assegnamo un valore alla proprietà. In questo punto possiamo fare controlli importanti:

  • verificare che il valore sia numerico;
  • impedire saldi negativi;
  • convertire il valore in float per uniformità.

Questo meccanismo è fondamentale per mantenere lo stato dell’oggetto coerente anche quando il codice esterno prova a modificarlo.

4. Metodi di dominio: deposita, preleva, trasferisci

I metodi deposita, preleva e trasferisci rappresentano operazioni reali del dominio bancario. Questo è un buon esempio di progettazione orientata agli oggetti: la classe non serve solo a “contenere dati”, ma anche a definire comportamenti validi.

In particolare:

  • deposita accetta solo importi positivi;
  • preleva controlla che il saldo sia sufficiente;
  • trasferisci usa i metodi già esistenti, evitando duplicazione di logica.

Quest’ultimo punto è una best practice importante: quando possibile, riutilizza i metodi della classe invece di riscrivere la stessa logica in più punti.

5. Il metodo __str__

Il metodo __str__ definisce come l’oggetto viene mostrato quando usiamo print(). Anche se questo tutorial non è focalizzato sui metodi speciali, è utile vedere come rendere l’oggetto più leggibile durante il debugging e nei messaggi di log.

Best practice

Quando progetti classi che devono mantenere uno stato coerente, segui queste buone pratiche:

  • Valida i dati in ingresso: non fidarti mai dei valori passati dall’esterno.
  • Nascondi i dettagli interni: usa attributi interni come _saldo e offri un’interfaccia chiara.
  • Evita la duplicazione: se un controllo è già presente in un metodo, riutilizzalo in altri metodi.
  • Usa nomi espliciti: metodi come deposita e preleva sono molto più chiari di nomi generici come aggiorna.
  • Preferisci eccezioni specifiche: TypeError e ValueError aiutano a capire subito il problema.
  • Progetta intorno alle regole del dominio: la classe deve riflettere le regole del mondo reale che rappresenta.

Un altro consiglio importante: se la logica di validazione cresce troppo, valuta di separarla in componenti più piccole. Una classe dovrebbe avere una responsabilità chiara e ben definita.

Riepilogo

In questo tutorial abbiamo visto come usare le classi in Python non solo per rappresentare dati, ma per proteggere e mantenere coerente lo stato di un oggetto. Abbiamo costruito una classe ContoBancario con proprietà, validazione e metodi operativi sicuri.

I concetti chiave da ricordare sono:

  • gli attributi interni possono essere gestiti con convenzioni come _nome;
  • @property permette di esporre un accesso controllato agli attributi;
  • i setter sono il punto ideale per validare i dati;
  • i metodi della classe dovrebbero rispettare le regole del dominio;
  • riutilizzare la logica interna rende il codice più pulito e affidabile.

Questa tecnica è molto utile in progetti reali: gestionali, e-commerce, applicazioni finanziarie, sistemi di prenotazione e in generale ovunque serva evitare stati incoerenti.

Approfondisci con risorse ufficiali

SHARE