Dataclasses são um recurso introduzido no Python 3.7 (PEP 557) que fornece uma maneira concisa e legível de criar classes que são essencialmente contêineres de dados. Com o decorador @dataclass, você evita escrever métodos repetitivos como __init__, __repr__, __eq__ e outros, que são gerados automaticamente. Isso torna o código mais limpo e menos propenso a erros.

Nesta aula, exploraremos os principais recursos das dataclasses, desde a definição básica até opções avançadas como campos imutáveis e comparação com estruturas de dados tradicionais.

@dataclass

O decorador @dataclass é aplicado a uma classe e, com base nas anotações de tipo dos atributos, gera automaticamente métodos especiais. Para usar dataclasses, primeiro importe o módulo: from dataclasses import dataclass.

Exemplo básico:

from dataclasses import dataclass

@dataclass
class Pessoa:
    nome: str
    idade: int
    email: str

# Uso
p = Pessoa(nome="Alice", idade=30, email="alice@example.com")
print(p)  # Pessoa(nome='Alice', idade=30, email='alice@example.com')
print(repr(p))  # Pessoa(nome='Alice', idade=30, email='alice@example.com')
print(p == Pessoa("Alice", 30, "alice@example.com"))  # True

Note que não foi necessário definir __init__, __repr__ ou __eq__. A dataclass gera esses métodos automaticamente. A ordem dos campos na definição da classe determina a ordem dos argumentos no construtor.

Campos e defaults

Você pode atribuir valores padrão aos campos, tornando-os opcionais na criação. No entanto, campos com valor padrão devem vir após campos sem padrão (como em funções).

from dataclasses import dataclass

@dataclass
class Produto:
    nome: str
    preco: float = 0.0
    quantidade: int = 0

p1 = Produto("Caneta")  # preco=0.0, quantidade=0
p2 = Produto("Caderno", 15.50, 10)
print(p1)  # Produto(nome='Caneta', preco=0.0, quantidade=0)
print(p2)  # Produto(nome='Caderno', preco=15.5, quantidade=10)

É possível usar tipos mutáveis como padrão, mas com cuidado. Para listas, dicionários ou outros objetos mutáveis, use field(default_factory=list) para evitar que o mesmo objeto seja compartilhado entre instâncias.

from dataclasses import dataclass, field
from typing import List

@dataclass
class Pedido:
    itens: List[str] = field(default_factory=list)

p1 = Pedido()
p1.itens.append("item1")
p2 = Pedido()
print(p2.itens)  # [] (não compartilha a lista)

O módulo field() também permite configurar outros parâmetros como init, repr, compare e hash.

frozen

Definir @dataclass(frozen=True) torna a classe imutável: após a criação, os atributos não podem ser alterados. Isso é útil para objetos que representam valores constantes ou para garantir que não haja modificações acidentais.

from dataclasses import dataclass

@dataclass(frozen=True)
class Ponto:
    x: float
    y: float

p = Ponto(1.0, 2.0)
# p.x = 3.0  # Isso gera um erro: dataclasses.FrozenInstanceError

Com classes congeladas, você pode usá-las como chaves em dicionários e em conjuntos, desde que todos os campos sejam hashable. O método __hash__ é gerado automaticamente se eq=True (padrão) e frozen=True. Se frozen=False, o hash não é gerado (a classe não é hashable).

vs namedtuple e dict

Antes das dataclasses, era comum usar namedtuple (do módulo collections) ou dicionários para armazenar dados. Vamos comparar:

  • namedtuple: cria tuplas com nomes de campos. São imutáveis e compactas, mas não permitem valores padrão, métodos personalizados ou herança facilmente. Exemplo: Ponto = namedtuple('Ponto', ['x', 'y']).
  • dict: flexível, mas sem validação de tipos, sem nomes de campos explícitos (acesso via strings) e sem métodos especiais como __repr__ automático.
  • dataclass: oferece o melhor dos dois mundos: imutabilidade opcional, valores padrão, métodos automáticos, suporte a herança e anotações de tipo. Além disso, é mais legível e fácil de manter.

Exemplo comparativo:

from collections import namedtuple

# namedtuple
PontoNT = namedtuple('PontoNT', ['x', 'y'])
p1 = PontoNT(1, 2)
print(p1.x, p1.y)  # 1 2
# p1.x = 3  # Erro: imutável

# dicionário
d = {'x': 1, 'y': 2}
print(d['x'], d['y'])  # 1 2
d['x'] = 3  # mutável

# dataclass
@dataclass(frozen=True)
class PontoDC:
    x: float
    y: float

p2 = PontoDC(1, 2)
print(p2.x, p2.y)  # 1 2
# p2.x = 3  # Erro se frozen=True

Dataclasses são preferíveis quando você precisa de mais funcionalidades, como métodos personalizados, herança ou validação. Para casos simples e imutáveis, namedtuple pode ser suficiente. Dicionários são úteis para estruturas dinâmicas, mas perdem em segurança de tipos e clareza.

Boas práticas

  • Sempre use anotações de tipo para todos os campos, mesmo que sejam tipos básicos.
  • Evite valores padrão mutáveis sem default_factory.
  • Prefira frozen=True para objetos que não devem ser alterados.
  • Use field(repr=False) para campos sensíveis (como senhas) que não devem aparecer no __repr__.
  • Para validação de dados, considere usar __post_init__.

Referências

Exercícios

  1. Crie uma dataclass chamada Livro com campos: titulo (str), autor (str), ano (int) e preco (float) com valor padrão 0.0.

    ✓ Resposta:
    from dataclasses import dataclass
    
    @dataclass
    class Livro:
        titulo: str
        autor: str
        ano: int
        preco: float = 0.0
  2. Modifique a classe Livro para que seja imutável (frozen). Teste a imutabilidade tentando alterar um atributo.

    ✓ Resposta:
    from dataclasses import dataclass
    
    @dataclass(frozen=True)
    class Livro:
        titulo: str
        autor: str
        ano: int
        preco: float = 0.0
    
    livro = Livro("1984", "George Orwell", 1949)
    # livro.titulo = "Novo titulo"  # Isso gera FrozenInstanceError
  3. Crie uma dataclass Aluno com campos: nome (str), notas (List[float]) com valor padrão uma lista vazia (use default_factory).

    ✓ Resposta:
    from dataclasses import dataclass, field
    from typing import List
    
    @dataclass
    class Aluno:
        nome: str
        notas: List[float] = field(default_factory=list)
  4. Compare dataclass e namedtuple: crie uma namedtuple Ponto com campos x e y, e uma dataclass equivalente. Mostre como criar uma instância de cada.

    ✓ Resposta:
    from collections import namedtuple
    from dataclasses import dataclass
    
    # namedtuple
    PontoNT = namedtuple('PontoNT', ['x', 'y'])
    p_nt = PontoNT(1, 2)
    
    # dataclass
    @dataclass
    class PontoDC:
        x: float
        y: float
    
    p_dc = PontoDC(1, 2)
    
    print(p_nt, p_dc)  # PontoNT(x=1, y=2) PontoDC(x=1, y=2)
  5. Use o método __post_init__ para validar que o campo ano em Livro seja maior que 1900. Se não for, levante um ValueError.

    ✓ Resposta:
    from dataclasses import dataclass
    
    @dataclass
    class Livro:
        titulo: str
        autor: str
        ano: int
        preco: float = 0.0
    
        def __post_init__(self):
            if self.ano <= 1900:
                raise ValueError("Ano deve ser maior que 1900")
    
    # Teste
    # livro = Livro("Título", "Autor", 1800)  # ValueError