Antes de dataclasses, definir una clase que solo almacenara datos exigía escribir a mano __init__, __repr__, __eq__ y a veces __hash__. El módulo dataclasses, disponible desde Python 3.7, genera todo eso automáticamente a partir de las anotaciones de tipo.
@dataclass básico
from dataclasses import dataclass
@dataclass
class Punto:
x: float
y: float
# Python genera automáticamente:
# __init__(self, x: float, y: float)
# __repr__(self) -> "Punto(x=1.0, y=2.5)"
# __eq__(self, other) -> compara campo a campo
p1 = Punto(1.0, 2.5)
p2 = Punto(1.0, 2.5)
print(p1) # Punto(x=1.0, y=2.5)
print(p1 == p2) # True
print(p1 is p2) # False
from dataclasses import dataclass
@dataclass
class Producto:
nombre: str
precio: float
stock: int = 0 # valor por defecto
activo: bool = True
p = Producto("Teclado", 49.99)
print(p) # Producto(nombre='Teclado', precio=49.99, stock=0, activo=True)
field() para valores por defecto avanzados
No puedes usar listas o diccionarios mutables como valor por defecto directamente (Python lo prohíbe para evitar que todas las instancias compartan el mismo objeto). field(default_factory=...) lo resuelve.
from dataclasses import dataclass, field
@dataclass
class Carrito:
propietario: str
items: list[str] = field(default_factory=list)
metadatos: dict = field(default_factory=dict)
_id_interno: int = field(default=0, repr=False, compare=False)
c1 = Carrito("Ana")
c2 = Carrito("Luis")
c1.items.append("manzana")
print(c1.items) # ['manzana']
print(c2.items) # [] listas independientes
__post_init__ para validar datos
__post_init__ se ejecuta justo después de que __init__ ha asignado los campos. Es el lugar para validar, transformar o calcular valores derivados.
from dataclasses import dataclass
@dataclass
class Rango:
minimo: float
maximo: float
def __post_init__(self):
if self.minimo > self.maximo:
raise ValueError(f"minimo ({self.minimo}) > maximo ({self.maximo})")
self.amplitud = self.maximo - self.minimo # campo calculado
r = Rango(0.0, 10.0)
print(r.amplitud) # 10.0
# Rango(10.0, 0.0) # ValueError
from dataclasses import dataclass, field
@dataclass
class Empleado:
nombre: str
salario: float
nombre_normalizado: str = field(init=False) # no se pasa en __init__
def __post_init__(self):
self.nombre_normalizado = self.nombre.strip().title()
e = Empleado(" ana garcía ", 2800.0)
print(e.nombre_normalizado) # Ana García
frozen=True: inmutabilidad
Con frozen=True los campos no pueden modificarse después de la creación. Python genera __hash__ automáticamente, lo que permite usar instancias como claves de diccionario o elementos de conjunto.
from dataclasses import dataclass
@dataclass(frozen=True)
class Coordenada:
latitud: float
longitud: float
def distancia_al_origen(self) -> float:
return (self.latitud ** 2 + self.longitud ** 2) ** 0.5
c = Coordenada(40.4168, -3.7038)
# c.latitud = 0.0 # FrozenInstanceError
# Usable como clave de dict o en sets
rutas = {c: "Madrid"}
print(rutas[Coordenada(40.4168, -3.7038)]) # Madrid
slots=True: eficiencia en memoria (Python 3.10+)
Con slots=True, Python usa __slots__ en lugar del __dict__ habitual. Las instancias ocupan menos memoria y el acceso a atributos es más rápido.
from dataclasses import dataclass
import sys
@dataclass
class PuntoNormal:
x: float
y: float
@dataclass(slots=True)
class PuntoSlots:
x: float
y: float
n = PuntoNormal(1.0, 2.0)
s = PuntoSlots(1.0, 2.0)
print(sys.getsizeof(n)) # ~48 bytes (con __dict__)
print(sys.getsizeof(s)) # ~40 bytes (sin __dict__)
# Si creas millones de instancias, la diferencia importa
order=True y comparaciones automáticas
from dataclasses import dataclass
@dataclass(order=True)
class Version:
mayor: int
menor: int
parche: int
def __str__(self) -> str:
return f"{self.mayor}.{self.menor}.{self.parche}"
versiones = [Version(2, 0, 0), Version(1, 10, 3), Version(1, 9, 5)]
versiones.sort()
for v in versiones:
print(v)
# 1.9.5
# 1.10.3
# 2.0.0
asdict() y astuple() para serialización
from dataclasses import dataclass, asdict, astuple
import json
@dataclass
class Usuario:
nombre: str
email: str
edad: int
u = Usuario("Ana", "[email protected]", 30)
print(asdict(u)) # {'nombre': 'Ana', 'email': '[email protected]', 'edad': 30}
print(json.dumps(asdict(u))) # serialización directa a JSON
print(astuple(u)) # ('Ana', '[email protected]', 30)
La combinación más frecuente en proyectos reales es frozen=True para value objects inmutables (coordenadas, identificadores, configuración de solo lectura) y slots=True cuando se crean muchos objetos en colecciones grandes. Para structs mutables con validación, __post_init__ cubre la mayoría de los casos sin necesidad de recurrir a Pydantic.
