Classi Python più leggibili con le dataclass

by theArchitect
SHARE
Classi Python più leggibili con le dataclass
© Guida-HTML5.it

Introduzione

Quando si lavora con le classi e gli oggetti, è comune creare oggetti destinati soprattutto a contenere dati: un prodotto, un utente, una prenotazione o una configurazione. In questi casi, il codice può diventare ripetitivo. Spesso bisogna scrivere il metodo __init__(), il metodo __repr__() e, talvolta, anche confronti tra oggetti o valori predefiniti.

Python mette a disposizione il modulo dataclasses, introdotto nella versione 3.7, che semplifica la definizione di classi usate come contenitori di dati. Una dataclass rimane una classe Python a tutti gli effetti, ma può generare automaticamente diversi metodi utili.

Questo strumento è particolarmente utile quando vogliamo descrivere oggetti con una struttura chiara, tipizzata e facile da mantenere. Nel tutorial costruiremo un piccolo catalogo di prodotti, con validazione dei dati, valori predefiniti e confronto tra oggetti.

Codice completo

from dataclasses import dataclass, field
from decimal import Decimal
from typing import List


@dataclass
class Prodotto:
    nome: str
    prezzo: Decimal
    categoria: str = "Generale"
    tags: List[str] = field(default_factory=list)

    def applica_sconto(self, percentuale: Decimal) -> None:
        """Riduce il prezzo del prodotto applicando una percentuale."""
        if not 0 <= percentuale <= 100:
            raise ValueError("La percentuale deve essere compresa tra 0 e 100")

        sconto = self.prezzo * percentuale / Decimal("100")
        self.prezzo -= sconto

    def aggiungi_tag(self, tag: str) -> None:
        """Aggiunge un tag evitando duplicati."""
        tag_normalizzato = tag.strip().lower()

        if not tag_normalizzato:
            raise ValueError("Il tag non può essere vuoto")

        if tag_normalizzato not in self.tags:
            self.tags.append(tag_normalizzato)


@dataclass(frozen=True)
class CodicePromozionale:
    codice: str
    percentuale: Decimal


def main() -> None:
    computer = Prodotto(
        nome="Notebook professionale",
        prezzo=Decimal("1299.90"),
        categoria="Informatica"
    )

    computer.aggiungi_tag("Lavoro")
    computer.aggiungi_tag("Portatile")
    computer.aggiungi_tag("lavoro")

    print(computer)
    # Prodotto(nome=´Notebook professionale´,
    #          prezzo=Decimal(´1299.90´),
    #          categoria=´Informatica´,
    #          tags=[´lavoro´, ´portatile´])

    computer.applica_sconto(Decimal("10"))
    print(f"Prezzo scontato: {computer.prezzo}")

    promozione_a = CodicePromozionale("ESTATE10", Decimal("10"))
    promozione_b = CodicePromozionale("ESTATE10", Decimal("10"))

    print(promozione_a == promozione_b)
    # True

    try:
        computer.applica_sconto(Decimal("150"))
    except ValueError as errore:
        print(f"Errore: {errore}")


if __name__ == "__main__":
    main()

Spiegazione

Definire una dataclass

La decorazione @dataclass informa Python che la classe deve essere trattata come una struttura composta principalmente da dati. Gli attributi dichiarati nel corpo della classe diventano automaticamente parametri del costruttore.

Nel nostro esempio, questa definizione:

computer = Prodotto(
    nome="Notebook professionale",
    prezzo=Decimal("1299.90")
)

è possibile perché Python genera automaticamente un metodo __init__() equivalente, concettualmente, a:

def __init__(self, nome, prezzo, categoria="Generale", tags=...):
    self.nome = nome
    self.prezzo = prezzo
    self.categoria = categoria
    self.tags = tags

Le annotazioni di tipo, come nome: str o prezzo: Decimal, rendono il codice più comprensibile e aiutano gli strumenti di analisi statica. Da sole, però, non impediscono di assegnare un valore del tipo sbagliato durante l’esecuzione. Per una validazione rigorosa occorrono controlli espliciti o librerie specializzate.

Valori predefiniti mutabili

Per l’attributo tags usiamo field(default_factory=list). Questa scelta è importante perché una lista è mutabile. Se scrivessimo semplicemente tags: List[str] = [], più oggetti potrebbero condividere la stessa lista, generando comportamenti inattesi.

default_factory=list crea invece una nuova lista ogni volta che viene istanziato un Prodotto. La stessa tecnica può essere usata con dizionari, insiemi o altri oggetti mutabili.

Metodi e comportamento

Una dataclass non contiene soltanto attributi. Possiamo aggiungere normali metodi di istanza, come applica_sconto() e aggiungi_tag(). Questo consente di mantenere vicino ai dati anche il comportamento che li riguarda.

Il metodo applica_sconto() controlla che la percentuale sia valida e modifica il prezzo. Per rappresentare valori monetari utilizziamo Decimal invece di float, evitando molti problemi di precisione tipici dei numeri in virgola mobile.

Rappresentazione e confronto

La decorazione genera automaticamente anche __repr__(), rendendo più leggibili le stampe e i messaggi di debug. Inoltre, per impostazione predefinita, le dataclass generano confronti basati sui valori degli attributi. Due prodotti con gli stessi dati, quindi, possono risultare uguali.

La classe CodicePromozionale usa invece @dataclass(frozen=True). In questo modo, dopo la creazione, gli attributi non possono essere modificati. È una scelta utile per oggetti che devono rappresentare valori immutabili, come codici, configurazioni o coordinate.

Best practice

  • Usa le dataclass per i dati strutturati: sono ideali quando una classe descrive un’entità con attributi ben definiti.
  • Non inserire valori mutabili direttamente come default: preferisci sempre default_factory per liste, dizionari e insiemi.
  • Scegli tipi appropriati: per importi economici usa Decimal, mentre per testo, numeri e date usa tipi coerenti con il dominio.
  • Valida i dati nei metodi o nel costruttore: le annotazioni di tipo migliorano la leggibilità, ma non sostituiscono i controlli a runtime.
  • Valuta l’immutabilità: frozen=True riduce il rischio di modifiche accidentali e rende gli oggetti più prevedibili.
  • Evita classi troppo generiche: una dataclass dovrebbe rappresentare un concetto preciso, non diventare un contenitore casuale di attributi.
  • Usa nomi espliciti: attributi come prezzo, categoria e tags comunicano meglio il significato dei dati.

Riepilogo

Le dataclass permettono di creare classi Python concise, leggibili e adatte a rappresentare dati strutturati. La decorazione @dataclass genera automaticamente il costruttore, la rappresentazione testuale e, in molti casi, i confronti tra oggetti.

Abbiamo visto come dichiarare attributi con annotazioni di tipo, gestire valori predefiniti mutabili con default_factory, aggiungere metodi di istanza e rendere una classe immutabile con frozen=True. Abbiamo inoltre utilizzato Decimal per gestire correttamente i prezzi.

Le dataclass non sostituiscono tutte le classi tradizionali: quando una classe contiene molte regole complesse, ereditarietà particolare o inizializzazione personalizzata, può essere più adatto scrivere manualmente i metodi. Per molti modelli applicativi, però, rappresentano una soluzione semplice e professionale.

Approfondisci con risorse ufficiali

SHARE