namedtuple y dataclass resuelven el mismo problema: estructurar datos relacionados en un objeto con campos nombrados, sin tener que escribir una clase completa a mano. Pero lo hacen de formas distintas, con diferentes compromisos en mutabilidad, herencia y funcionalidades extra.
namedtuple: tupla con campos nombrados
from collections import namedtuple
# Define el tipo y sus campos
Punto = namedtuple("Punto", ["x", "y"])
Color = namedtuple("Color", "rojo verde azul") # también con string
p = Punto(3, 4)
print(p.x, p.y) # 3 4
print(p[0], p[1]) # 3 4 también por índice
print(p) # Punto(x=3, y=4)
# Son inmutables: p.x = 10 lanzaría AttributeError
# Son tuplas: len(p), p == (3, 4), list(p)
# _replace crea una copia con campos modificados
p2 = p._replace(x=10)
print(p2) # Punto(x=10, y=4)
print(p) # Punto(x=3, y=4) original sin cambios
namedtuple: valores por defecto (Python 3.6.1+)
from collections import namedtuple
# defaults cubre los últimos N campos desde la derecha
Config = namedtuple("Config", ["host", "puerto", "ssl", "timeout"],
defaults=["localhost", 5432, True, 30])
c1 = Config()
print(c1) # Config(host='localhost', puerto=5432, ssl=True, timeout=30)
c2 = Config("produccion.ejemplo.com", ssl=False)
print(c2) # Config(host='produccion.ejemplo.com', puerto=5432, ssl=False, timeout=30)
dataclass: clases de datos modernas
from dataclasses import dataclass, field
@dataclass
class Empleado:
nombre: str
email: str
salario: float = 30000.0
habilidades: list = field(default_factory=list) # evita el bug de mutable compartido
e = Empleado("Ana", "[email protected]", salario=45000.0)
e.habilidades.append("Python")
print(e)
# Empleado(nombre='Ana', email='[email protected]', salario=45000.0, habilidades=['Python'])
# __eq__ generado automáticamente
e2 = Empleado("Ana", "[email protected]", salario=45000.0)
print(e == e2) # False (habilidades difieren)
dataclass frozen: inmutabilidad como namedtuple
from dataclasses import dataclass
@dataclass(frozen=True)
class Punto:
x: float
y: float
def distancia_origen(self):
return (self.x**2 + self.y**2) ** 0.5
p = Punto(3, 4)
print(p.distancia_origen()) # 5.0
# p.x = 10 # FrozenInstanceError
# frozen=True habilita __hash__, así que pueden usarse en sets y dicts
puntos = {Punto(0, 0), Punto(1, 1), Punto(0, 0)}
print(puntos) # {Punto(x=0, y=0), Punto(x=1, y=1)}
dataclass con __post_init__: validación
from dataclasses import dataclass
@dataclass
class Transaccion:
concepto: str
importe: float
tipo: str = "debito" # 'debito' o 'credito'
def __post_init__(self):
if self.importe <= 0:
raise ValueError(f"El importe debe ser positivo: {self.importe}")
if self.tipo not in ("debito", "credito"):
raise ValueError(f"Tipo no válido: {self.tipo}")
# Normalizar
self.concepto = self.concepto.strip()
t = Transaccion(" Pago alquiler ", 800.0)
print(t.concepto) # 'Pago alquiler'
Cuándo usar cada uno
# namedtuple: # + Inmutable por defecto (seguro para usar como clave de dict) # + Ligero: es una tupla, ocupa menos memoria # + Interop con código que espera tuplas # - Sin métodos propios (salvo con herencia) # - Validación limitada # dataclass: # + Mutable por defecto (o frozen=True para inmutabilidad) # + Validación en __post_init__ # + field(default_factory=...) para mutables # + Herencia limpia # + Conversión a dict con asdict() # - Algo más verboso # - No es una tupla
Para datos simples que nunca cambian y que pueden viajar entre funciones, namedtuple es la opción más ligera. Para datos con lógica de validación, campos opcionales complejos o métodos propios, las dataclass son la herramienta moderna.
