dataclasses en Python: @dataclass, field(), frozen, slots y __post_init__

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.

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP