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:
classExercise:def__init__(self, name, reps, sets, weight):
self.name = name
self.reps = reps
self.sets = sets
self.weight = weight
Offerto da
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
@dataclassclassExercise:
name:str
reps:int
sets:int
weight:float# Weight in lbsOfferto da
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
@dataclassclassExercise:
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'Offerto da
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)"Offerto da
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ì:
classExercise: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>Offerto da
Sembra davvero brutto e inutile!
Ora verifichiamo l'esistenza di __eq__, che è l'operatore di uguaglianza:
# Redefine the class@dataclassclassExercise:
name:str
reps:int
sets:int
weight:float
ex1 = Exercise("Bench press",10,3,52.5)
ex2 = Exercise("Bench press",10,3,52.5)Offerto da
Confrontare la classe con se stessa e con un'altra classe con parametri identici deve restituire 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
@dataclassclassDummy:
attr: AnyOfferto da
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”Offerto da
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:
@dataclassclassExercise:
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)Offerto da
Ricorda che i campi senza valore predefinito non possono seguire campi con valore predefinito. Per esempio, il codice sotto genererà un errore:
@dataclassclassExercise:
name:str="Push-ups"
reps:int=10
sets:int=3
weight:float# NOT ALLOWED
ex5 = Exercise()
ex5
TypeError: non-default argument 'weight' follows default argumentOfferto da
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
@dataclassclassExercise:
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)Offerto da
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)Offerto da
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
@dataclassclassExercise:
name:str="Push-ups"
reps:int=10
sets:int=3
weight:float=0@dataclassclassWorkoutSession:
exercises: List[Exercise]
duration_minutes:intOfferto da
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)Offerto da
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:
@dataclassclassWorkoutSession:
exercises: List[Exercise]=[]
duration_minutes:int=None
hiit_monday = WorkoutSession("25-02-2024")
ValueError: mutable default <class'list'>for field exercises isnot allowed: use default_factoryOfferto da
Tuttavia, abbiamo ottenuto un errore: le data class non consentono valori predefiniti mutabili.
Per fortuna, possiamo risolvere usando una default factory:
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:
defcreate_warmup():return[
Exercise("Jumping jacks",30,1),
Exercise("Squat lunges",10,2),
Exercise("High jumps",20,1),]@dataclassclassWorkoutSession:
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)Offerto da
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:
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)Offerto da
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__:
@dataclassclassExercise:
name:str="Push-ups"
reps:int=10
sets:int=3
weight:float=0def__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)Offerto da
Il __repr__ è ancora lo stesso, ma quando chiamiamo print su di esso:
La rappresentazione stringa della classe è molto più gradevole. Ora sistemiamo anche WorkoutSession:
@dataclassclassWorkoutSession:
exercises: List[Exercise]= field(default_factory=create_warmup)
duration_minutes:int=5# Increase the default duration as welldefadd_exercise(self, exercise: Exercise):
self.exercises.append(exercise)defincrease_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.Offerto da
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'Offerto da
Riceviamo un TypeError perché le data class non implementano gli operatori di confronto. Ma si può risolvere facilmente impostando il parametro order su 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ì:
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:
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
5Offerto da
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:
@dataclassclassWorkoutSession:
exercises: List[Exercise]= field(default_factory=create_warmup)
duration_minutes:int= field(default=0, init=False)def__post_init__(self):
set_duration =3for ex in self.exercises:
self.duration_minutes += ex.sets * set_duration
...Offerto da
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
9Offerto da
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
1000del hiit_friday.exercisesOfferto da
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)classFrozenExercise:
name:str
reps:int
sets:int
weight:int|float=0
ex1 = FrozenExercise("Muscle-ups",5,3)Offerto da
Ora, se vogliamo modificare un campo, otteniamo un errore:
ex1.sets =5
FrozenInstanceError: cannot assign to field 'sets'Offerto da
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'Offerto da
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)classImmutableWorkoutSession:
exercises: List[Exercise]= field(default_factory=create_warmup)
duration_minutes:int=5
session1 = ImmutableWorkoutSession()Offerto da
Come previsto, non possiamo modificare direttamente la lista degli esercizi:
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)Offerto da
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)classImmutableWorkoutSession:
exercises: List[Exercise]= field(default_factory=create_warmup)
duration_minutes:int=5@dataclass(frozen=True)classCardioWorkoutSession(ImmutableWorkoutSession):passOfferto da
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)classImmutableWorkoutSession:
exercises: List[Exercise]= field(default_factory=create_warmup)
duration_minutes:int=5@dataclass(frozen=True)classCardioWorkoutSession(ImmutableWorkoutSession):
intensity_level:str# Not allowed, must have a default
TypeError: non-default argument 'intensity_level' follows default argument
Offerto da
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.
Scopri i 15 migliori server MCP remoti che stanno trasformando lo sviluppo AI nel 2026. Scopri come migliorano automazione, ragionamento, sicurezza e velocità dei workflow.
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:
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:
Questo pezzetto di codice dall'aspetto modesto è di gran lunga migliore di una classe normale. Il piccolo decorator
@dataclassimplementa 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:Al momento,
Exerciseha già i metodi__repr__e__eq__implementati. Verifichiamolo:La rappresentazione di un oggetto con
reprdeve restituire il codice in grado di ricrearlo, e possiamo vedere che è esattamente il caso diex1.In confronto,
Exercisedefinita nel vecchio modo apparirebbe così:Sembra davvero brutto e inutile!
Ora verifichiamo l'esistenza di
__eq__, che è l'operatore di uguaglianza:Confrontare la classe con se stessa e con un'altra classe con parametri identici deve restituire 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 tipoAny: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
Exercisecon tipi di dato completamente errati può essere eseguito senza errori: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:
Ricorda che i campi senza valore predefinito non possono seguire campi con valore predefinito. Per esempio, il codice sotto genererà un errore:
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:La funzione
fieldha altri parametri, come:reprinitcomparedefault_factorye 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: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
WorkoutSessionche accetta due campi:Usando il tipo
List, stiamo specificando cheWorkoutSessionaccetta una lista di istanze diExercise.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:Tuttavia, abbiamo ottenuto un errore: le data class non consentono valori predefiniti mutabili.
Per fortuna, possiamo risolvere usando una default factory:
Il parametro
default_factoryaccetta una funzione che restituisce un valore iniziale per un campo della data class. Questo significa che può accettare qualsiasi funzione arbitraria:tupledictsetQuesto è 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
Exercisedi riscaldamento:Ora, ogni volta che creiamo una sessione, arriverà con alcuni esercizi di riscaldamento già registrati. La nuova versione di
WorkoutSessionha 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:Usando questi metodi, ora possiamo registrare qualsiasi nuova attività in una sessione:
Ma c'è un problema:
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 classLe 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__:Il
__repr__è ancora lo stesso, ma quando chiamiamoprintsu di esso:La rappresentazione stringa della classe è molto più gradevole. Ora sistemiamo anche
WorkoutSession: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:
Riceviamo un
TypeErrorperché le data class non implementano gli operatori di confronto. Ma si può risolvere facilmente impostando il parametroordersuTrue: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ì:
Come previsto, abbiamo ottenuto
False.Ma cosa succederebbe se il primo campo di
Workoutfosse di un altro tipo, diciamo una stringa? Proviamo a scoprirlo: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:Impostando
comparesuFalseper 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?
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_minutesdipendente dal campoexercises.Implementiamolo:
Questa volta stiamo definendo
duration_minutesconinitimpostato suFalseper 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 ogniExercise.Ora, quando inizializziamo
WorkoutSession,duration_minutesaumenta dinamicamente di tre minuti per ogni serie in ciascun esercizio.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:Vogliamo proteggere tutti i campi delle nostre classi in modo che possano essere modificati solo come vogliamo. Per farlo, il decorator
@dataclassoffre un comodo argomentofrozen:Ora, se vogliamo modificare un campo, otteniamo un errore:
Impostare
frozensuTrueaggiunge 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: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
WorkoutSessionconfrozenimpostato suTrue:Come previsto, non possiamo modificare direttamente la lista degli esercizi:
Tuttavia,
exercisesè una lista, quindi pienamente mutabile, il che rende possibile la seguente operazione:# Modificare uno degli elementi in una lista
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:
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:
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:
__init__personalizzati__new__personalizzatiE 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
defaultdicte inamedtuplenell'ultimo capitolo del corso Data Types for Data Science.