Blog

¿dataclass, clase, NamedTuple o dict?

Cuatro formas de guardar datos estructurados en Python. La pregunta que decide no es de estilo: es si un error de tipeo debería fallar en voz alta.

Python te da cuatro formas razonables de guardar unos cuantos valores relacionados. Elegir mal rara vez es fatal y siempre es un poco molesto, así que vale la pena dedicarle diez minutos una vez.

Las cuatro

d = {'x': 3, 'y': 4}
print(d, d['x'])
print('typo is silent:', d.get('z'))
try:
    d['z']
except KeyError as err:
    print('KeyError:', err)
{'x': 3, 'y': 4} 3
typo is silent: None
KeyError: 'z'
class P1:
    def __init__(self, x, y): self.x, self.y = x, y
print(P1(3,4).x)

@dataclass
class P2:
    x: int
    y: int
print(P2(3,4), P2(3,4) == P2(3,4))

class P3(NamedTuple):
    x: int
    y: int
p3 = P3(3,4)
print(p3, p3.x, tuple(p3), p3 == (3,4))
3
P2(x=3, y=4) True
P3(x=3, y=4) 3 (3, 4) True

La dataclass te da __init__, __repr__ y __eq__ a partir de las anotaciones. El NamedTuple te da todo eso más inmutabilidad, desempaquetado de tuplas, e igualdad con tuplas normales.

Mutabilidad

p2 = P2(3,4); p2.x = 99
print('dataclass mutable:', p2)
try:
    p3.x = 99
except AttributeError as err:
    print('NamedTuple immutable:', type(err).__name__ + ':', err)

@dataclass(frozen=True)
class P4:
    x: int
    y: int
print('frozen dataclass hashable:', hash(P4(3,4)) == hash(P4(3,4)))
dataclass mutable: P2(x=99, y=4)
NamedTuple immutable: AttributeError: can't set attribute
frozen dataclass hashable: True

frozen=True te da una dataclass con las garantías de un NamedTuple, y de paso la posibilidad de usarla como clave.

La pregunta que decide: qué pasa con un error de tipeo

Esta es la que de verdad importa en el día a día.

p = P2(3,4)
p.z = 5                      # a plain/dataclass object accepts anything
print('dataclass accepts a typo:', p.z)
try:
    P3(3,4).z
except AttributeError as err:
    print('NamedTuple rejects it:', type(err).__name__ + ':', err)
dataclass accepts a typo: 5
NamedTuple rejects it: 'P3' object has no attribute 'z'

Un diccionario devuelve None desde .get() sin decir nada. Una clase normal o una dataclass acepta en silencio un atributo nuevo que nunca declaraste: p.z = 5 no es un error, así que una asignación mal escrita crea un campo que nadie lee. Un NamedTuple rechaza las dos cosas.

Ese es el argumento más fuerte a favor de NamedTuple o de una dataclass congelada para los datos que pasas de un lado a otro: el error de tipeo falla donde lo cometiste, no tres funciones después. La parte 10 muestra cómo __slots__ le da la misma protección a una clase mutable.

Memoria

print('dict       ', sys.getsizeof({'x':3,'y':4}))
print('NamedTuple ', sys.getsizeof(P3(3,4)))
print('class      ', sys.getsizeof(P1(3,4)) + sys.getsizeof(P1(3,4).__dict__))
print('dataclass  ', sys.getsizeof(P2(3,4)) + sys.getsizeof(P2(3,4).__dict__))
dict        184
NamedTuple  56
class       328
dataclass   304

Un NamedTuple es una tupla con nombres, así que no carga un __dict__ por instancia. Las clases y las dataclasses sí, y eso domina su tamaño.

Toma estos números como una forma, no como un benchmark: varían según la versión y según lo que guardes. La forma es estable: el NamedTuple es mucho más chico, el diccionario queda en medio, y los objetos con __dict__ son los más grandes. Solo importa con cientos de miles de instancias.

Cómo elegir

dict — cuando la forma es genuinamente dinámica, viene de JSON, o no conoces las claves de antemano. Deja de convertir JSON externo en objetos por costumbre.

NamedTuple — un valor chico e inmutable que pasas de un lado a otro y comparas. El más barato, el más seguro contra errores de tipeo, y funciona con desempaquetado de tuplas.

dataclass — la opción por defecto para cualquier cosa que hayas escrito tú con más de unos tres campos, o que tenga que cambiar después de construirse. Agrega frozen=True salvo que tengas una razón para no hacerlo.

clase normal — cuando el comportamiento importa más que los datos: métodos, validación en __init__, properties calculadas. Esa es la cuenta bancaria de la parte 1, y es un trabajo distinto.

Qué recordar

  • Un error de tipeo en un diccionario es silencioso; en una clase o dataclass crea un campo en silencio.

  • NamedTuple y las dataclasses con frozen=True rechazan las dos cosas.

  • Recurre a una dataclass por defecto, con frozen=True salvo que algo tenga que mutar.

  • Deja el JSON externo como diccionario hasta que tengas una razón para darle un tipo.

¿Qué tan útil te resultó este post?

¡Haz clic en un corazón para calificar!

Calificación promedio 0 / 5. Total de votos: 0

Todavía no hay votos. Sé el primero en calificar este post.