W3docs

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. NamedTuple vs. 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: int

Entrambe 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 field

Il decoratore genera:

MetodoCosa 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 allowed

Usa 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 list

default_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:

ParametroScopo
defaultUn semplice valore predefinito (solo scalare)
default_factoryUn callable che produce il valore predefinito
reprFalse per escludere questo campo da __repr__
compareFalse per escludere questo campo da __eq__ (e dall'ordinamento)
initFalse 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) # True

Ordinamento

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)])   # London

Le 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 -1

Puoi 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)    # 24

Ereditarietà 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 argument

Aggira 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

CaratteristicaClasse sempliceNamedTupledataclass
__init__ automaticoNo
__repr__ automaticoNo
__eq__ automaticoNoSì (per valore)Sì (per valore)
MutabileNoSì (predefinito)
HashableNo (se __eq__ definito)Solo con frozen=True
OrdinamentoManualeorder=True
EreditarietàLimitata
Controllo isinstanceSì (anche tuple)
Unpacking (a, b = obj)NoNo

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.

ConcettoCosa fa
@dataclassGenera __init__, __repr__, __eq__ automaticamente
field()Controllo granulare dei campi: valori predefiniti, repr, compare, init
default_factoryFornisce un nuovo valore predefinito mutabile per ogni istanza
order=TrueAggiunge <, >, <=, >= in base all'ordine dei campi
frozen=TrueRende 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.

Pratica

Pratica
Which decorator do you use to create a dataclass in Python?
Which decorator do you use to create a dataclass in Python?
Was this page helpful?