Blog

dataclass, classe, NamedTuple ou dict?

O Python te dá quatro jeitos razoáveis de guardar alguns valores relacionados. Escolher mal raramente é fatal e sempre é levemente irritante, então vale gastar dez minutos uma vez.

Os quatro

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

A dataclass te dá __init__, __repr__ e __eq__ a partir das anotações. A NamedTuple te dá tudo isso mais imutabilidade, desempacotamento de tupla, e igualdade com tuplas comuns.

Mutabilidade

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 dá uma dataclass com as garantias de uma NamedTuple, e hashabilidade junto.

A pergunta que decide: o que acontece com um erro de digitação

Essa é a que realmente importa no dia a dia.

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'

Um dicionário devolve None em silêncio no .get(). Uma classe comum ou dataclass silenciosamente aceita um atributo novo que você nunca declaroup.z = 5 não é erro, então uma atribuição com o nome errado cria um campo que ninguém lê. Uma NamedTuple recusa os dois.

Esse é o argumento mais forte a favor de NamedTuple ou dataclass congelada para dados que circulam: o erro de digitação falha onde você o cometeu, não três funções adiante. A parte 10 mostra como __slots__ dá a uma classe mutável a mesma proteção.

Memória

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

Uma NamedTuple é uma tupla com nomes, então ela não carrega um __dict__ por instância. Classes e dataclasses carregam, e isso domina o tamanho delas.

Trate esses números como um formato, não como benchmark — eles variam por versão e pelo que está guardado. O formato é estável: NamedTuple é bem menor, dicionário fica no meio, objetos com __dict__ são os maiores. Só importa na casa das centenas de milhares de instâncias.

Como escolher

dict — o formato é genuinamente dinâmico, vem de JSON, ou você não sabe as chaves de antemão. Pare de converter JSON externo em objetos por hábito.

NamedTuple — um valor pequeno e imutável que você passa adiante e compara. O mais barato, o mais seguro contra erros de digitação, funciona com desempacotamento de tupla.

dataclass — o padrão para qualquer coisa que você mesmo escreveu com mais de uns três campos, ou que precisa mudar depois de construída. Use frozen=True a menos que tenha um motivo para não usar.

classe comum — quando comportamento importa mais que os dados: métodos, validação no __init__, properties calculadas. Essa é a conta bancária da parte 1, e é outro trabalho.

O que lembrar

  • Um erro de digitação num dicionário é silencioso; numa classe ou dataclass ele cria um campo em silêncio.

  • NamedTuple e dataclasses com frozen=True recusam os dois.

  • Use dataclass por padrão, com frozen=True a menos que algo precise mudar.

  • Mantenha JSON externo como dicionário até ter um motivo para dar um tipo a ele.

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.