Corso Python / Creare classi di dati con dataclass
Dataclass genera metodi come inizializzazione, rappresentazione e confronto di uguaglianza a partire dai campi dichiarati in una classe. Riduce il codice ripetitivo dei record, ma non controlla automaticamente che gli ingressi rispettino le annotazioni di tipo o le regole dell’applicazione.
Ridurre il codice ripetitivo dei record
Il decoratore @dataclass individua i campi nelle annotazioni della classe e, con le opzioni predefinite, genera metodi come __init__, __repr__ e __eq__. Puoi quindi descrivere nome, importo ed etichette senza scrivere a mano tutta l’inizializzazione e la rappresentazione testuale.
La documentazione ufficiale di dataclasses descrive campi, opzioni e metodi generati. Non è un sostituto universale delle classi normali: è particolarmente utile quando la struttura del record è centrale, mentre oggetti con comportamenti complessi richiedono comunque una progettazione delle responsabilità.
Per un valore mutabile predefinito usa una funzione di creazione, come field(default_factory=list). List viene chiamata quando occorre il valore predefinito di ogni nuova istanza. In questo modo due record creati senza tags esplicito ricevono liste indipendenti, evitando il problema degli attributi mutabili condivisi.
Eseguire un record con etichette indipendenti
Salva il codice completo come data_records.py, in UTF-8, in una cartella di esercizio. Il modulo dataclasses fa parte della libreria standard; l’annotazione list[str] usata qui richiede Python 3.9 o successivo senza adattamenti. Dal terminale con l’interprete del progetto selezionato esegui python data_records.py. Fuori dall’ambiente virtuale su macOS o Ubuntu usa python3 se è il nome scelto.
from dataclasses import dataclass, field
@dataclass
class Expense:
name: str
amount: int
tags: list[str] = field(default_factory=list)
def __post_init__(self):
if type(self.amount) is not int or self.amount < 0:
raise ValueError("invalid amount")
a = Expense("lunch", 8000)
b = Expense("bus", 2500)
a.tags.append("work")
print(a)
print(b.tags)
print(a == Expense("lunch", 8000, ["work"]))
Il risultato previsto è:
Expense(name='lunch', amount=8000, tags=['work'])
[]
True
La prima riga è la rappresentazione generata dal dataclass: mostra classe e valori dei campi, inclusa la lista tags. Aggiungere work ad a non modifica b.tags, che resta vuota. L’ultima riga confronta a con un’altra istanza Expense avente gli stessi valori, quindi restituisce True.
La notazione name: str e amount: int descrive l’intento dei campi; la regola sull’importo viene invece eseguita dal codice scritto in __post_init__. Senza quella funzione il decoratore non aggiungerebbe da solo il rifiuto dei valori negativi.
Capire validazione iniziale e limiti di frozen
__post_init__ viene chiamato dopo l’inizializzatore generato. Nell’esempio rifiuta un importo che non sia esattamente un int non negativo, escludendo anche True e valori come 1.5. Non verifica però ogni altro campo: per esempio, non contiene una regola eseguibile che imponga a name di essere una stringa non vuota.
Il controllo iniziale non impedisce una successiva assegnazione come a.amount = -1. Se lo stato deve restare valido a ogni modifica, valuta una property con setter oppure un disegno in cui gli aggiornamenti producono nuovi record validati. Non confondere una verifica alla creazione con una garanzia su tutte le operazioni future.
L’opzione frozen=True limita la normale riassegnazione dei campi, ma non congela profondamente gli oggetti a cui essi fanno riferimento. Una lista contenuta in tags può ancora ricevere append anche se non puoi sostituire l’intero campo. Per uno stato immutabile scegli anche tipi interni appropriati, come tuple con elementi a loro volta coerenti con il contratto.
Default_factory interviene quando il chiamante non fornisce tags. Se passi esplicitamente la stessa lista a due record, entrambi possono conservarla e vedere le stesse modifiche. Il decoratore non copia automaticamente ogni argomento ricevuto: documenta quando vuoi condivisione e quando vuoi indipendenza.
Definire uguaglianza, identità e conversione
Il confronto generato considera i campi inclusi nel confronto e richiede istanze dello stesso tipo. Due oggetti distinti possono risultare uguali senza essere lo stesso oggetto; se modifichi un campo confrontato, il risultato può cambiare. La scelta di questo criterio tecnico non decide se due spese rappresentino la stessa transazione: per riconoscere eventi reali serve un’identità applicativa progettata appositamente.
dataclasses.asdict aiuta a trasformare il record in dati di dizionario, ma non risolve tutte le conversioni richieste da JSON. Date e Decimal possono richiedere ancora una rappresentazione esplicita. La guida ai dati JSON distingue trasformazione dei tipi, serializzazione e validazione.
- Esegui l’originale e verifica rappresentazione, lista vuota di b e confronto True.
- In prove separate crea Expense con un importo negativo e poi con True: entrambi devono generare ValueError con invalid amount. Con zero, la creazione deve riuscire.
- Crea due record senza tags esplicito e aggiungi un’etichetta soltanto al primo. Verifica che il secondo resti invariato.
- Confronta due record con gli stessi campi usando == e is: uguaglianza dei valori e identità dell’oggetto sono domande diverse. Ripristina infine l’esempio originale.
Prima di usare l’uguaglianza per eliminare presunti duplicati, stabilisci quali campi identificano davvero un evento. Prima di salvare un record, verifica invece che il formato scelto rappresenti tutti i dati necessari: ridurre il codice della classe non elimina queste decisioni.




