Vai al contenuto principale

Data Class in Python: un tutorial completo

Un tutorial per principianti sulle data class di Python e su come usarle in pratica
Aggiornato 3 giu 2026  · 9 min leggi

Le data class sono una di quelle funzionalità di Python che, una volta scoperte, ti fanno dimenticare per sempre il vecchio modo di fare. Considera questa classe normale:

class Exercise:
   def __init__(self, name, reps, sets, weight):
       self.name = name
       self.reps = reps
       self.sets = sets
       self.weight = weight

Per me, quella definizione di classe è molto inefficiente: nel metodo __init__ ripeti ogni parametro almeno tre volte. Può non sembrare un gran problema, ma pensa a quante volte nella tua vita scriverai classi con molti più parametri.

In confronto, dai un'occhiata all'alternativa con le data class del codice qui sopra:

from dataclasses import dataclass


@dataclass
class Exercise:
   name: str
   reps: int
   sets: int
   weight: float  # Weight in lbs

Questo pezzetto di codice dall'aspetto modesto è di gran lunga migliore di una classe normale. Il piccolo decorator @dataclass implementa dietro le quinte le classi __init__, __repr__, __eq__, che a mano avrebbero richiesto almeno 20 righe di codice.

Inoltre, molte altre funzionalità, come gli operatori di confronto, l'ordinamento degli oggetti e l'immutabilità, sono a una sola riga di distanza dall'essere create magicamente per la nostra classe.

Quindi, lo scopo di questo tutorial è mostrarti perché le data class sono tra le cose migliori capitate a Python se ti piace la programmazione orientata agli oggetti.

Iniziamo!

Basi delle data class in Python

Copriamo alcune nozioni fondamentali delle data class di Python che le rendono così utili.

Alcuni metodi sono generati automaticamente nelle data class

Nonostante tutte le loro funzionalità, le data class sono classi normali che richiedono molto meno codice per implementare la stessa logica. Ecco di nuovo la classe Exercise:

from dataclasses import dataclass


@dataclass
class Exercise:
   name: str
   reps: int
   sets: int
   weight: float


ex1 = Exercise("Bench press", 10, 3, 52.5)

# Verifying Exercise is a regular class
ex1.name
'Bench press'

Al momento, Exercise ha già i metodi __repr__ e __eq__ implementati. Verifichiamolo:

repr(ex1)
"Exercise(name='Bench press', reps=10, sets=3, weight=52.5)"

La rappresentazione di un oggetto con repr deve restituire il codice in grado di ricrearlo, e possiamo vedere che è esattamente il caso di ex1.

In confronto, Exercise definita nel vecchio modo apparirebbe così:

class Exercise:
   def __init__(self, name, reps, sets, weight):
       self.name = name
       self.reps = reps
       self.sets = sets
       self.weight = weight


ex3 = Exercise("Bench press", 10, 3, 52.5)

ex3
<__main__.Exercise at 0x7f6834100130>

Sembra davvero brutto e inutile!

Ora verifichiamo l'esistenza di __eq__, che è l'operatore di uguaglianza:

# Redefine the class
@dataclass
class Exercise:
   name: str
   reps: int
   sets: int
   weight: float


ex1 = Exercise("Bench press", 10, 3, 52.5)
ex2 = Exercise("Bench press", 10, 3, 52.5)

Confrontare la classe con se stessa e con un'altra classe con parametri identici deve restituire True:

ex1 == ex2
True
ex1 == ex1
True

E infatti! Nelle classi normali, questa logica sarebbe stata noiosa da scrivere.

Le data class richiedono gli hint di tipo

Come avrai notato, le data class richiedono gli hint di tipo quando si definiscono i campi. In effetti, le data class accettano qualsiasi tipo dal modulo typing. Per esempio, ecco come creare un campo che possa accettare il tipo Any:

from typing import Any


@dataclass
class Dummy:
   attr: Any

Tuttavia, un'idiosincrasia di Python è che, sebbene le data class richiedano gli hint di tipo, i tipi non vengono effettivamente applicati.

Per esempio, creare un'istanza della classe Exercise con tipi di dato completamente errati può essere eseguito senza errori:

silly_exercise = Exercise("Bench press", "ten", "three sets", 52.5)

silly_exercise.sets

“Three sets”

Se vuoi far rispettare i tipi, devi usare type checker come Mypy.

Le data class consentono valori predefiniti nei campi

Finora non abbiamo aggiunto alcun valore predefinito alle nostre classi. Sistemiamo subito:

@dataclass
class Exercise:
   name: str = "Push-ups"
   reps: int = 10
   sets: int = 3
   weight: float = 0


# Now, all fields have defaults
ex5 = Exercise()
ex5
Exercise(name='Push-ups', reps=10, sets=3, weight=0)

Ricorda che i campi senza valore predefinito non possono seguire campi con valore predefinito. Per esempio, il codice sotto genererà un errore:

@dataclass
class Exercise:
   name: str = "Push-ups"
   reps: int = 10
   sets: int = 3
   weight: float  # NOT ALLOWED


ex5 = Exercise()
ex5
TypeError: non-default argument 'weight' follows default argument

In pratica, raramente definirai i default con la sintassi name: type = value.

Al contrario, userai la funzione field, che permette un maggiore controllo su ogni definizione di campo:

from dataclasses import field


@dataclass
class Exercise:
   name: str = field(default="Push-up")
   reps: int = field(default=10)
   sets: int = field(default=3)
   weight: float = field(default=0)


# Now, all fields have defaults
ex5 = Exercise()
ex5
Exercise(name='Push-up', reps=10, sets=3, weight=0)

La funzione field ha altri parametri, come:

  • repr
  • init
  • compare
  • default_factory

e così via. Ne parleremo nelle prossime sezioni.

Le data class possono essere create con una funzione

Un'ultima nota sulle basi delle data class: la loro definizione può essere ancora più breve usando la funzione make_dataclass:

from dataclasses import make_dataclass

Exercise = make_dataclass(
   "Exercise",
   [
       ("name", str),
       ("reps", int),
       ("sets", int),
       ("weight", float),
   ],
)

ex3 = Exercise("Deadlifts", 8, 3, 69.0)
ex3
Exercise(name='Deadlifts', reps=8, sets=3, weight=69.0)

Ma sacrificherai la leggibilità, quindi non consiglio di usare questa funzione.

Data class avanzate in Python

In questa sezione parleremo di funzionalità avanzate delle data class che portano ulteriori vantaggi. Una di queste è la factory di default.

Default factory

Per spiegare le factory di default, creiamo un'altra classe chiamata WorkoutSession che accetta due campi:

from dataclasses import dataclass
from typing import List


@dataclass
class Exercise:
   name: str = "Push-ups"
   reps: int = 10
   sets: int = 3
   weight: float = 0


@dataclass
class WorkoutSession:
   exercises: List[Exercise]
   duration_minutes: int

Usando il tipo List, stiamo specificando che WorkoutSession accetta una lista di istanze di Exercise.

# Define the Exercise instances for HIIT training
ex1 = Exercise(name="Burpees", reps=15, sets=3)
ex2 = Exercise(name="Mountain Climbers", reps=20, sets=3)
ex3 = Exercise(name="Jump Squats", reps=12, sets=3)
exercises_monday = [ex1, ex2, ex3]

hiit_monday = WorkoutSession(exercises=exercises_monday, duration_minutes=30)

Al momento, ogni istanza di workout richiede che gli esercizi siano inizializzati. Ma questo non rispecchia come si allenano le persone: prima avviano una sessione (probabilmente in un'app) e poi aggiungono gli esercizi man mano che si allenano.

Quindi dobbiamo poter creare sessioni senza esercizi e senza durata. Facciamolo aggiungendo una lista vuota come valore predefinito per exercises:

@dataclass
class WorkoutSession:
   exercises: List[Exercise] = []
   duration_minutes: int = None


hiit_monday = WorkoutSession("25-02-2024")
ValueError: mutable default <class 'list'> for field exercises is not allowed: use default_factory

Tuttavia, abbiamo ottenuto un errore: le data class non consentono valori predefiniti mutabili.

Per fortuna, possiamo risolvere usando una default factory:

@dataclass
class WorkoutSession:
   exercises: List[Exercise] = field(default_factory=list)  # PAY ATTENTION
   duration_minutes: int = 0


hiit_monday = WorkoutSession()
hiit_monday
WorkoutSession(exercises=[], duration_minutes=0)

Il parametro default_factory accetta una funzione che restituisce un valore iniziale per un campo della data class. Questo significa che può accettare qualsiasi funzione arbitraria:

  • tuple
  • dict
  • set
  • Qualsiasi funzione definita dall'utente

Questo è vero indipendentemente dal fatto che il risultato della funzione sia mutabile o meno.

Ora, se ci pensi, la maggior parte delle persone inizia l'allenamento con esercizi di riscaldamento che sono tipicamente simili per qualsiasi tipo di workout. Quindi inizializzare le sessioni senza esercizi potrebbe non essere ciò che alcuni vogliono.

Invece, creiamo una funzione che restituisca tre Exercise di riscaldamento:

def create_warmup():
   return [
       Exercise("Jumping jacks", 30, 1),
       Exercise("Squat lunges", 10, 2),
       Exercise("High jumps", 20, 1),
   ]

@dataclass
class WorkoutSession:
   exercises: List[Exercise] = field(default_factory=create_warmup)
   duration_minutes: int = 5  # Increase the default duration as well


hiit_monday = WorkoutSession()
hiit_monday

WorkoutSession(exercises=[Exercise(name='Jumping jacks', reps=30, sets=1, weight=0), Exercise(name='Squat lunges', reps=10, sets=2, weight=0), Exercise(name='High jumps', reps=20, sets=1, weight=0)], duration_minutes=5)

Ora, ogni volta che creiamo una sessione, arriverà con alcuni esercizi di riscaldamento già registrati. La nuova versione di WorkoutSession ha una durata predefinita di cinque minuti per tenerne conto.

Aggiungere metodi alle data class

Dato che le data class sono classi normali, aggiungere metodi funziona allo stesso modo. Aggiungiamo due metodi alla nostra data class WorkoutSession:

@dataclass
class WorkoutSession:
   exercises: List[Exercise] = field(default_factory=create_warmup)
   duration_minutes: int = 5

   def add_exercise(self, exercise: Exercise):
       self.exercises.append(exercise)

   def increase_duration(self, minutes: int):
       self.duration_minutes += minutes

Usando questi metodi, ora possiamo registrare qualsiasi nuova attività in una sessione:

hiit_monday = WorkoutSession()

# Log a new exercise
new_exercise = Exercise("Deadlifts", 6, 4, 60)

hiit_monday.add_exercise(new_exercise)
hiit_monday.increase_duration(15)

Ma c'è un problema:

hiit_monday

WorkoutSession(exercises=[Exercise(name='Jumping jacks', reps=30, sets=1, weight=0), Exercise(name='Squat lunges', reps=10, sets=2, weight=0), Exercise(name='High jumps', reps=20, sets=1, weight=0), Exercise(name='Deadlifts', reps=6, sets=4, weight=60)], duration_minutes=20)

Quando stampiamo la sessione, la sua rappresentazione predefinita è troppo verbosa e illeggibile, poiché contiene il codice per ricreare l'oggetto. Sistemiamo.

__repr__ e __str__ nelle data class

Le data class implementano automaticamente __repr__ ma non __str__. Questo fa sì che la classe ricada su __repr__ quando la stampiamo con print.

Quindi, sovrascriviamo questo comportamento definendo noi stessi __str__:

@dataclass
class Exercise:
   name: str = "Push-ups"
   reps: int = 10
   sets: int = 3
   weight: float = 0

   def __str__(self):
       base = f"{self.name}: {self.reps}/{self.sets}"
       if self.weight == 0:
           return base
       return base + f", {self.weight} lbs"


ex1 = Exercise(name="Burpees", reps=15, sets=3)
ex1
Exercise(name='Burpees', reps=15, sets=3, weight=0)

Il __repr__ è ancora lo stesso, ma quando chiamiamo print su di esso:

print(ex1)
Burpees: 15/3

La rappresentazione stringa della classe è molto più gradevole. Ora sistemiamo anche WorkoutSession:

@dataclass
class WorkoutSession:
   exercises: List[Exercise] = field(default_factory=create_warmup)
   duration_minutes: int = 5  # Increase the default duration as well

   def add_exercise(self, exercise: Exercise):
       self.exercises.append(exercise)

   def increase_duration(self, minutes: int):
       self.duration_minutes += minutes

   def __str__(self):
       base = ""

       for ex in self.exercises:
           base += str(ex) + "\n"
       base += f"\nSession duration: {self.duration_minutes} minutes."

       return base


hiit_monday = WorkoutSession()
print(hiit_monday)

Jumping jacks: 30/1
Squat lunges: 10/2
High jumps: 20/1

Session duration: 5 minutes.

Nota: usa il pulsante “Spiega il codice” in fondo allo snippet per ottenere una spiegazione riga per riga del codice.

Ora abbiamo un output leggibile e compatto.

Confronto nelle data class

Per molte classi ha senso confrontare i loro oggetti secondo una certa logica. Per i workout, può essere la durata, l'intensità dell'esercizio o il peso.

Per prima cosa, vediamo cosa succede se proviamo a confrontare due allenamenti nello stato attuale:

hiit_wednesday = WorkoutSession()

hiit_wednesday.add_exercise(Exercise("Pull-ups", 7, 3))
print(hiit_wednesday)

Jumping jacks: 30/1
Squat lunges: 10/2
High jumps: 20/1
Pull-ups: 7/3

Session duration: 5 minutes.

hiit_monday > hiit_wednesday
TypeError: '>' not supported between instances of 'WorkoutSession' and 'WorkoutSession'

Riceviamo un TypeError perché le data class non implementano gli operatori di confronto. Ma si può risolvere facilmente impostando il parametro order su True:

@dataclass(order=True)
class WorkoutSession:
   exercises: List[Exercise] = field(default_factory=create_warmup)
   duration_minutes: int = 5

   ...

hiit_monday = WorkoutSession()
# hiit_monday.add_exercise(...)
hiit_monday.increase_duration(10)

hiit_wednesday = WorkoutSession()

hiit_monday > hiit_wednesday

True

Questa volta il confronto funziona, ma cosa stiamo confrontando esattamente?

Nelle data class, il confronto viene eseguito nell'ordine in cui sono definiti i campi. Al momento, le classi vengono confrontate in base alla durata dell'allenamento, dato che il primo campo, exercises, contiene oggetti non standard.

Possiamo verificarlo aumentando la durata della sessione del mercoledì:

hiit_monday = WorkoutSession()
# hiit_monday.add_exercise(...)

hiit_wednesday = WorkoutSession()
hiit_wednesday.increase_duration(10)

hiit_monday > hiit_wednesday
False

Come previsto, abbiamo ottenuto False.

Ma cosa succederebbe se il primo campo di Workout fosse di un altro tipo, diciamo una stringa? Proviamo a scoprirlo:

@dataclass(order=True)
class WorkoutSession:
   date: str = None  # DD-MM-YYYY
   exercises: List[Exercise] = field(default_factory=create_warmup)
   duration_minutes: int = 5

   ...

hiit_monday = WorkoutSession("25-02-2024")
hiit_monday.increase_duration(10)

hiit_wednesday = WorkoutSession("27-02-2024")

hiit_monday > hiit_wednesday
False

Anche se la sessione di lunedì dura di più, il confronto ci dice che è minore di quella di mercoledì. Il motivo è che “25” viene prima di “27” nel confronto tra stringhe in Python.

Quindi, come manteniamo l'ordine dei campi e allo stesso tempo ordiniamo le sessioni in base alla durata dell'allenamento? È facile con la funzione field:

@dataclass(order=True)
class WorkoutSession:
   date: str = field(default=None, compare=False)
   exercises: List[Exercise] = field(default_factory=create_warmup)
   duration_minutes: int = 5

   ...

hiit_monday = WorkoutSession("25-02-2024")
hiit_monday.increase_duration(10)

hiit_wednesday = WorkoutSession("27-02-2024")

hiit_monday > hiit_wednesday
True

Impostando compare su False per un campo, lo escludiamo dall'ordinamento, come dimostra il risultato sopra.

Manipolazione dei campi post-init

Al momento, abbiamo una durata predefinita della sessione di cinque minuti per tener conto degli esercizi di riscaldamento. Tuttavia, questo ha senso solo se un utente avvia una sessione con un riscaldamento. E se iniziasse con altri esercizi?

new_session = WorkoutSession([Exercise("Diamond push-ups", 10, 3)])

new_session.duration_minutes
5

Per un solo esercizio, la durata totale è di cinque minuti, il che è illogico. Ogni sessione deve stimare dinamicamente la sua durata in base al numero di serie di ciascun esercizio. Questo significa che dovremmo rendere duration_minutes dipendente dal campo exercises.

Implementiamolo:

@dataclass
class WorkoutSession:
   exercises: List[Exercise] = field(default_factory=create_warmup)
   duration_minutes: int = field(default=0, init=False)

   def __post_init__(self):
       set_duration = 3
       for ex in self.exercises:
           self.duration_minutes += ex.sets * set_duration

   ...

Questa volta stiamo definendo duration_minutes con init impostato su False per ritardare l'inizializzazione del campo.

Poi, all'interno del metodo speciale __post_init__, aggiorniamo il suo valore in base al numero totale di serie in ogni Exercise.

Ora, quando inizializziamo WorkoutSession, duration_minutes aumenta dinamicamente di tre minuti per ogni serie in ciascun esercizio.

# Adding an exercise with three sets
hiit_friday = WorkoutSession([Exercise("Diamond push-ups", 10, 3)])

hiit_friday.duration_minutes
9

In generale, se vuoi definire un campo che dipende da altri campi della tua data class, puoi usare la logica di __post_init__.

Immutabilità nelle data class

La nostra data class WorkoutSession è quasi pronta; va solo protetta. Al momento, può essere rovinata piuttosto facilmente:

hiit_friday.duration_minutes = 1000

hiit_friday.duration_minutes
1000

del hiit_friday.exercises

Vogliamo proteggere tutti i campi delle nostre classi in modo che possano essere modificati solo come vogliamo. Per farlo, il decorator @dataclass offre un comodo argomento frozen:

@dataclass(frozen=True)
class FrozenExercise:
   name: str
   reps: int
   sets: int
   weight: int | float = 0


ex1 = FrozenExercise("Muscle-ups", 5, 3)

Ora, se vogliamo modificare un campo, otteniamo un errore:

ex1.sets = 5
FrozenInstanceError: cannot assign to field 'sets'

Impostare frozen su True aggiunge automaticamente i metodi __deleteattr__ e __setattr__ per ogni campo, così che siano protetti da eliminazioni o aggiornamenti dopo l'inizializzazione. Inoltre, non sarà possibile aggiungere nuovi campi:

ex1.new_field = 10
FrozenInstanceError: cannot assign to field 'new_field'

Questa funzionalità richiederebbe decine di righe di codice se stessimo usando classi tradizionali.

Tieni però presente che non possiamo rendere le nostre classi veramente immutabili. Per esempio, riscriviamo WorkoutSession con frozen impostato su True:

@dataclass(frozen=True)
class ImmutableWorkoutSession:
   exercises: List[Exercise] = field(default_factory=create_warmup)
   duration_minutes: int = 5


session1 = ImmutableWorkoutSession()

Come previsto, non possiamo modificare direttamente la lista degli esercizi:

session1.exercises = [Exercise()]

Tuttavia, exercises è una lista, quindi pienamente mutabile, il che rende possibile la seguente operazione:

# Modificare uno degli elementi in una lista

# Changing one of the elements in a list
session1.exercises[1] = FrozenExercise("Totally new exercise", 5, 5)

print(session1)

ImmutableWorkoutSession(exercises=[Exercise(name='Jumping jacks', reps=30, sets=1, weight=0), FrozenExercise(name='Totally new exercise', reps=5, sets=5, weight=0), Exercise(name='High jumps', reps=20, sets=1, weight=0)], duration_minutes=5)

Quindi, per proteggersi da modifiche accidentali, si consiglia di usare oggetti immutabili come le tuple per i valori dei campi.

Ereditarietà nelle data class

Un ultimo punto che tratteremo è l'ordine dei campi nelle classi genitore e figlie.

Poiché le data class sono classi normali, l'ereditarietà funziona come al solito:

@dataclass(frozen=True)
class ImmutableWorkoutSession:
   exercises: List[Exercise] = field(default_factory=create_warmup)
   duration_minutes: int = 5


@dataclass(frozen=True)
class CardioWorkoutSession(ImmutableWorkoutSession):
   pass

Ma, dato che l'ultimo campo nella classe genitore (ImmutableWorkoutSession) ha un valore predefinito, tutti i campi nelle classi figlie devono avere valori predefiniti.

Per esempio, questo non è consentito:

@dataclass(frozen=True)
class ImmutableWorkoutSession:
   exercises: List[Exercise] = field(default_factory=create_warmup)
   duration_minutes: int = 5


@dataclass(frozen=True)
class CardioWorkoutSession(ImmutableWorkoutSession):
   intensity_level: str  # Not allowed, must have a default

TypeError: non-default argument 'intensity_level' follows default argument

Svantaggi delle data class e risorse aggiuntive

Le data class sono migliorate costantemente da Python 3.7 (erano ottime già all'inizio) e coprono molti casi d'uso in cui potresti dover scrivere classi. Ma possono essere svantaggiose nei seguenti scenari:

  • Metodi __init__ personalizzati
  • Metodi __new__ personalizzati
  • Vari pattern di ereditarietà

E molti altri, come discusso in questo ottimo thread su Reddit. Se vuoi una motivazione più dettagliata sul perché sono state introdotte le data class e perché non sono sostituti diretti delle definizioni di classi tradizionali, leggi PEP 557.

Se ti interessa la programmazione orientata agli oggetti in generale, ecco un corso per proseguire il tuo percorso:

Fondamentalmente, le data class sono strutture più eleganti per conservare e recuperare i dati in modo più efficiente. Tuttavia, Python ha molte altre strutture dati che svolgono questo compito in modo più o meno simile. Per esempio, puoi imparare a conoscere i counter, i defaultdict e i namedtuple nell'ultimo capitolo del corso Data Types for Data Science.


Bex Tuychiev's photo
Author
Bex Tuychiev
LinkedIn

Sono un creator di contenuti sulla data science con oltre 2 anni di esperienza e uno dei profili con più seguito su Medium. Mi piace scrivere articoli dettagliati su AI e ML con un pizzico di sarcasmo, perché qualcosa bisogna pur fare per renderli un po' meno noiosi. Ho pubblicato più di 130 articoli e anche un corso su DataCamp, con un altro in arrivo. I miei contenuti sono stati visti da oltre 5 milioni di occhi, e 20.000 di loro sono diventati follower sia su Medium che su LinkedIn. 

Argomenti

Continua a imparare Python

Programma

Fondamenti di dati in Python

28 h
Accresci le tue competenze sui dati, scopri come manipolarli e visualizzarli e applica analisi avanzate per prendere decisioni basate sui dati.
Vedi dettagliRight Arrow
Inizia il corso
Mostra altroRight Arrow