Blog

¿dataclass, clase, NamedTuple o dict?

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.

How useful was this post?

Click on a heart to rate it!

Average rating 0 / 5. Vote count: 0

No votes so far! Be the first to rate this post.