Python Dataclasses
Impara le dataclasses di Python: il decoratore @dataclass, i valori predefiniti con field(), ordinamento, immutabilità ed ereditarietà con esempi pratici.
Una dataclass è una normale classe Python il cui codice ripetitivo — __init__, __repr__ e __eq__ — viene generato automaticamente dal decoratore @dataclass. Il risultato è meno codice, meno errori di battitura e classi immediatamente leggibili.
Questo capitolo tratta:
- Perché esistono le dataclasses e quando usarle
- Il decoratore
@dataclass - Valori predefiniti dei campi e l'helper
field() - Controllo dell'uguaglianza e dell'ordinamento
- Dataclasses immutabili con
frozen=True - Logica di post-inizializzazione con
__post_init__ - Ereditarietà con le dataclasses
- Dataclasses vs.
NamedTuplevs. classi semplici
Prima di leggere questo capitolo, assicurati di avere familiarità con classi e oggetti Python e l'ereditarietà Python.
Perché le Dataclasses?
Considera una classe che memorizza un prodotto in un negozio online. Senza dataclasses, scrivi le stesse assegnazioni di attributi tre volte — una in __init__, una in __repr__ e una in __eq__:
class Product:
def __init__(self, name, price, stock):
self.name = name
self.price = price
self.stock = stock
def __repr__(self):
return f"Product(name={self.name!r}, price={self.price}, stock={self.stock})"
def __eq__(self, other):
if not isinstance(other, Product):
return NotImplemented
return (self.name, self.price, self.stock) == (other.name, other.price, other.stock)Il decoratore @dataclass genera tutto quanto sopra da un singolo elenco annotato di campi:
from dataclasses import dataclass
@dataclass
class Product:
name: str
price: float
stock: intEntrambe le versioni si comportano in modo identico. La versione con dataclass è più breve, più difficile da sbagliare e comunica immediatamente che questa classe è principalmente un contenitore di dati.
Il Decoratore @dataclass
Importa dataclass dal modulo dataclasses della libreria standard e applicalo alla tua classe. Ogni campo viene dichiarato come variabile di classe con annotazione di tipo:
from dataclasses import dataclass
@dataclass
class Point:
x: float
y: float
p = Point(1.5, 2.0)
print(p) # Point(x=1.5, y=2.0)
print(p.x) # 1.5
p2 = Point(1.5, 2.0)
print(p == p2) # True — __eq__ compares field by fieldIl decoratore genera:
| Metodo | Cosa fa |
|---|---|
__init__ | Accetta ogni campo come parametro e lo assegna a self |
__repr__ | Restituisce una stringa leggibile come Point(x=1.5, y=2.0) |
__eq__ | Confronta due istanze campo per campo |
Le annotazioni di tipo sono richieste ma non applicate a runtime
Le dichiarazioni di campo richiedono un'annotazione di tipo (x: float). Python non verifica il tipo a runtime — puoi comunque passare una string dove è atteso un float. L'annotazione è metadato usato dai type-checker come mypy e dalla stessa macchina dataclasses. Per la validazione del tipo a runtime, vedi Python Type Hints.
Valori Predefiniti
Assegna un valore predefinito direttamente al campo per renderlo opzionale in __init__:
from dataclasses import dataclass
@dataclass
class Config:
host: str = "localhost"
port: int = 8080
debug: bool = False
c1 = Config()
print(c1) # Config(host='localhost', port=8080, debug=False)
c2 = Config(host="example.com", port=443)
print(c2) # Config(host='example.com', port=443, debug=False)I campi con valori predefiniti devono apparire dopo i campi senza valori predefiniti — esattamente la stessa regola che vale per i parametri delle funzioni normali.
Valori predefiniti mutabili e field()
Non puoi usare un object mutabile (una lista, un dict, o un set) come valore predefinito diretto. Python condividerebbe una lista tra tutte le istanze, il che porta a bug sottili:
from dataclasses import dataclass
# This raises a ValueError at class definition time:
# @dataclass
# class Bag:
# items: list = [] # ValueError: mutable default is not allowedUsa invece field(default_factory=...) per creare un nuovo object per ogni istanza:
from dataclasses import dataclass, field
@dataclass
class Bag:
items: list = field(default_factory=list)
b1 = Bag()
b2 = Bag()
b1.items.append("apple")
print(b1.items) # ['apple']
print(b2.items) # [] — b2 has its own separate listdefault_factory accetta qualsiasi callable senza argomenti, incluse le lambda e le tue funzioni personalizzate.
L'Helper field()
field() ti offre un controllo granulare sui singoli campi. I suoi parametri più utili sono:
| Parametro | Scopo |
|---|---|
default | Un semplice valore predefinito (solo scalare) |
default_factory | Un callable che produce il valore predefinito |
repr | False per escludere questo campo da __repr__ |
compare | False per escludere questo campo da __eq__ (e dall'ordinamento) |
init | False per escludere questo campo da __init__ |
from dataclasses import dataclass, field
import time
@dataclass
class LogEntry:
message: str
level: str = "INFO"
timestamp: float = field(default_factory=time.time, repr=False, compare=False)
entry = LogEntry("Server started")
print(entry) # LogEntry(message='Server started', level='INFO')
# timestamp exists but is hidden from repr and ignored in comparisons
print(entry.timestamp > 0) # TrueOrdinamento
Per impostazione predefinita le dataclasses supportano l'uguaglianza (==, !=) ma non l'ordinamento (<, >, <=, >=). Abilita l'ordinamento passando order=True al decoratore:
from dataclasses import dataclass
@dataclass(order=True)
class Version:
major: int
minor: int
patch: int
v1 = Version(1, 2, 0)
v2 = Version(1, 3, 0)
v3 = Version(1, 2, 0)
print(v1 < v2) # True
print(v1 == v3) # True
print(v2 > v1) # True
versions = [Version(2, 0, 0), Version(1, 9, 1), Version(1, 2, 3)]
print(sorted(versions))
# [Version(major=1, minor=2, patch=3),
# Version(major=1, minor=9, patch=1),
# Version(major=2, minor=0, patch=0)]Python genera i metodi di confronto confrontando i campi nell'ordine in cui sono dichiarati, in stile tupla. Puoi escludere un campo dai confronti con field(compare=False).
Dataclasses Immutabili con frozen=True
Passa frozen=True per rendere tutti i campi in sola lettura dopo la creazione. Qualsiasi tentativo di modificare un campo genera un FrozenInstanceError:
from dataclasses import dataclass
@dataclass(frozen=True)
class Coordinate:
lat: float
lon: float
london = Coordinate(51.5074, -0.1278)
print(london) # Coordinate(lat=51.5074, lon=-0.1278)
# london.lat = 0.0 # FrozenInstanceError: cannot assign to field 'lat'Le dataclasses frozen sono anche hashable (implementano __hash__), quindi puoi usarle come chiavi di dizionario o elementi di set:
from dataclasses import dataclass
@dataclass(frozen=True)
class Coordinate:
lat: float
lon: float
cities = {
Coordinate(51.5074, -0.1278): "London",
Coordinate(48.8566, 2.3522): "Paris",
}
print(cities[Coordinate(51.5074, -0.1278)]) # LondonLe dataclasses normali (mutabili) non sono hashable per impostazione predefinita — Python imposta __hash__ a None quando __eq__ è definito senza frozen=True.
Logica di Post-Inizializzazione con __post_init__
A volte è necessario derivare il valore di un campo da altri campi, o validare l'input dopo l'esecuzione di __init__. Definisci un metodo __post_init__ — viene chiamato automaticamente alla fine dell'__init__ generato:
from dataclasses import dataclass, field
import math
@dataclass
class Circle:
radius: float
def __post_init__(self):
if self.radius <= 0:
raise ValueError(f"radius must be positive, got {self.radius}")
@property
def area(self):
return math.pi * self.radius ** 2
c = Circle(5)
print(round(c.area, 4)) # 78.5398
# Circle(-1) # ValueError: radius must be positive, got -1Puoi anche calcolare un campo derivato. Contrassegnalo con field(init=False) in modo che non compaia in __init__, poi impostalo all'interno di __post_init__:
from dataclasses import dataclass, field
@dataclass
class Rectangle:
width: float
height: float
area: float = field(init=False, repr=True)
def __post_init__(self):
self.area = self.width * self.height
r = Rectangle(4, 6)
print(r) # Rectangle(width=4, height=6, area=24)
print(r.area) # 24Ereditarietà con le Dataclasses
Una dataclass può ereditare da un'altra dataclass. L'__init__ della classe figlia include i campi di entrambe le classi — prima i campi del genitore, nell'ordine in cui sono stati dichiarati:
from dataclasses import dataclass
@dataclass
class Animal:
name: str
age: int
@dataclass
class Dog(Animal):
breed: str
rex = Dog(name="Rex", age=3, breed="Labrador")
print(rex) # Dog(name='Rex', age=3, breed='Labrador')Attenzione: se una classe genitore ha un campo con un valore predefinito, tutti i campi della classe figlia devono avere anch'essi dei valori predefiniti. È la stessa regola che si applica alle firme delle funzioni Python normali — un parametro senza valore predefinito non può seguire uno con valore predefinito.
from dataclasses import dataclass
@dataclass
class Animal:
name: str
age: int = 0 # has a default
# @dataclass
# class Dog(Animal):
# breed: str # TypeError: non-default argument 'breed' follows default argumentAggira questo problema assegnando un valore predefinito anche al campo della classe figlia, oppure ristrutturando la gerarchia in modo che i campi con valori predefiniti vengano per ultimi.
Parametri del Decoratore in Sintesi
@dataclass(
init=True, # generate __init__ (default True)
repr=True, # generate __repr__ (default True)
eq=True, # generate __eq__ (default True)
order=False, # generate <, >, <=, >= (default False)
frozen=False, # make fields immutable (default False)
)
class MyClass:
...Raramente è necessario modificare la maggior parte di questi. I più comuni sono order=True e frozen=True.
Funzioni di Utilità
Il modulo dataclasses fornisce anche tre utili funzioni:
fields()
Restituisce una tupla di object Field che descrivono ogni campo nella classe:
from dataclasses import dataclass, fields
@dataclass
class Point:
x: float
y: float
for f in fields(Point):
print(f.name, f.type)
# x <class 'float'>
# y <class 'float'>asdict()
Converte un'istanza di dataclass in un dizionario semplice (ricorsivamente):
from dataclasses import dataclass, asdict
@dataclass
class Address:
street: str
city: str
@dataclass
class Person:
name: str
address: Address
p = Person("Alice", Address("10 Downing St", "London"))
print(asdict(p))
# {'name': 'Alice', 'address': {'street': '10 Downing St', 'city': 'London'}}Questo è utile per la serializzazione in JSON o per l'invio di dati a un'API.
astuple()
Converte in una tupla (ricorsivamente):
from dataclasses import dataclass, astuple
@dataclass
class Point:
x: float
y: float
p = Point(3.0, 4.0)
print(astuple(p)) # (3.0, 4.0)Dataclasses vs. NamedTuple vs. Classi Semplici
| Caratteristica | Classe semplice | NamedTuple | dataclass |
|---|---|---|---|
__init__ automatico | No | Sì | Sì |
__repr__ automatico | No | Sì | Sì |
__eq__ automatico | No | Sì (per valore) | Sì (per valore) |
| Mutabile | Sì | No | Sì (predefinito) |
| Hashable | No (se __eq__ definito) | Sì | Solo con frozen=True |
| Ordinamento | Manuale | Sì | order=True |
| Ereditarietà | Sì | Limitata | Sì |
Controllo isinstance | Sì | Sì (anche tuple) | Sì |
Unpacking (a, b = obj) | No | Sì | No |
Usa una dataclass quando:
- Vuoi dati mutabili con immutabilità opzionale.
- Hai bisogno di ereditarietà o logica post-init.
- Vuoi un controllo granulare dei campi (
field()).
Usa NamedTuple quando:
- Vuoi un record immutabile che si comporti anche come tupla (unpacking posizionale, righe CSV).
- Hai bisogno di compatibilità con codice che si aspetta tuple.
Usa una classe semplice quando:
- La classe ha un comportamento significativo e pochissimi dati semplici.
- Hai bisogno di un
__init__personalizzato che non può essere espresso tramite__post_init__.
Errori Comuni
Valori predefiniti mutabili. Usare una lista o un dict come valore predefinito diretto genera un ValueError al momento della definizione della classe. Usa sempre field(default_factory=...).
Hashing. Le dataclasses normali non sono hashable. Se hai bisogno di usarle come chiavi di dict o in set, usa frozen=True oppure passa unsafe_hash=True (raramente consigliato).
eq=False. Se disabiliti la generazione dell'uguaglianza (eq=False), Python torna al confronto per identità (is), che quasi mai è quello che si vuole per object dati.
Ordinamento dei valori predefiniti ereditati. Se un campo del genitore ha un valore predefinito e un campo del figlio no, Python genera un TypeError. Pianifica attentamente l'ordine dei campi nella tua gerarchia.
Riepilogo
| Concetto | Cosa fa |
|---|---|
@dataclass | Genera __init__, __repr__, __eq__ automaticamente |
field() | Controllo granulare dei campi: valori predefiniti, repr, compare, init |
default_factory | Fornisce un nuovo valore predefinito mutabile per ogni istanza |
order=True | Aggiunge <, >, <=, >= in base all'ordine dei campi |
frozen=True | Rende i campi in sola lettura e l'istanza hashable |
__post_init__ | Viene eseguito dopo __init__ per la validazione o i campi derivati |
fields() | Restituisce metadati su ogni campo |
asdict() | Converte l'istanza in un dizionario semplice (ricorsivamente) |
astuple() | Converte l'istanza in una tupla semplice (ricorsivamente) |
Per argomenti correlati, vedi classi e oggetti Python, ereditarietà Python e classi base astratte Python.