Dataclasses: simplificando classes de dados
1. Introdução às Dataclasses
Classes de dados são um padrão comum em Python: estruturas que servem principalmente para armazenar valores, com pouca ou nenhuma lógica de negócio. Tradicionalmente, escrevê-las exigia muito código boilerplate — __init__, __repr__, __eq__ — tornando o código verboso e propenso a erros.
# Classe tradicional - muito boilerplate
class Pessoa:
def __init__(self, nome: str, idade: int, email: str):
self.nome = nome
self.idade = idade
self.email = email
def __repr__(self):
return f"Pessoa(nome='{self.nome}', idade={self.idade}, email='{self.email}')"
def __eq__(self, other):
if not isinstance(other, Pessoa):
return NotImplemented
return (self.nome, self.idade, self.email) == (other.nome, other.idade, other.email)
Com @dataclass, introduzido no Python 3.7 (PEP 557), isso se reduz drasticamente:
from dataclasses import dataclass
@dataclass
class Pessoa:
nome: str
idade: int
email: str
O decorador gera automaticamente __init__, __repr__, __eq__ e __hash__ (se apropriado), baseado nas anotações de tipo. Menos código, menos bugs, mais legibilidade.
2. Configuração e Parâmetros do Decorador
O decorador @dataclass aceita parâmetros que controlam quais métodos especiais são gerados:
@dataclass(init=True, repr=True, eq=True, order=False, frozen=False, slots=False)
class Config:
debug: bool = False
timeout: int = 30
init: Controla a geração do__init__. Útil quando você quer definir construtores personalizados.repr: Gera__repr__automático. Desative se precisar de representação customizada.eq: Gera__eq__. Essencial para comparações entre instâncias.order: QuandoTrue, gera__lt__,__le__,__gt__,__ge__, permitindo ordenação.frozen: Torna a instância imutável (como uma tupla nomeada, mas mais flexível).slots(Python 3.10+): Gera__slots__, economizando memória e acelerando acesso a atributos.
@dataclass(order=True)
class Produto:
nome: str
preco: float
p1 = Produto("Mouse", 50.0)
p2 = Produto("Teclado", 120.0)
print(p1 < p2) # True (compara por nome, depois preço)
3. Campos e Tipos com field()
A função field() oferece controle granular sobre cada campo:
from dataclasses import dataclass, field
from typing import List
@dataclass
class Pedido:
itens: List[str] = field(default_factory=list)
desconto: float = field(default=0.0, repr=False)
id_pedido: int = field(init=False, default=0)
def __post_init__(self):
self.id_pedido = hash(tuple(self.itens))
Argumentos principais de field():
- default: Valor padrão para o campo.
- default_factory: Função sem argumentos que gera o valor padrão (necessário para tipos mutáveis como listas).
- init: Se False, o campo não aparece no __init__.
- repr: Se False, o campo é omitido do __repr__.
- compare: Se False, o campo é ignorado em comparações.
- hash: Se False, o campo não participa do cálculo de hash.
@dataclass
class Usuario:
nome: str
email: str = field(repr=False) # Não exibe email em logs
permissoes: set = field(default_factory=set) # Valor padrão mutável
4. Herança e Ordem dos Campos
Dataclasses suportam herança, mas a ordem dos campos segue regras específicas:
@dataclass
class Base:
x: int = 0
y: str = "default"
@dataclass
class Derivada(Base):
z: float = 0.0
x: int = 10 # Sobrescreve campo da base
d = Derivada() # x=10, y="default", z=0.0
Campos da classe base vêm primeiro, seguidos pelos campos da subclasse. InitVar permite variáveis de inicialização que não são armazenadas como campos:
from dataclasses import InitVar
@dataclass
class Conexao:
host: str
porta: int
timeout: InitVar[float] = 30.0 # Só para inicialização
def __post_init__(self, timeout):
self._timeout_real = timeout if timeout > 0 else 10.0
InitVar é útil para processar dados temporários durante a construção sem poluir o estado do objeto.
5. Métodos Especiais e Comportamento Pós-Inicialização
__post_init__ é chamado automaticamente após __init__. Ideal para validações e transformações:
from datetime import datetime
import re
@dataclass
class UsuarioValidado:
nome: str
email: str
criado_em: datetime = field(init=False)
def __post_init__(self):
if not re.match(r"[^@]+@[^@]+\.[^@]+", self.email):
raise ValueError(f"Email inválido: {self.email}")
self.criado_em = datetime.now()
try:
u = UsuarioValidado("João", "email-invalido")
except ValueError as e:
print(e) # Email inválido: email-invalido
Com InitVar, podemos receber dados temporários e processá-los:
@dataclass
class Relatorio:
titulo: str
dados_brutos: InitVar[list]
dados_processados: list = field(init=False)
def __post_init__(self, dados_brutos):
self.dados_processados = [d for d in dados_brutos if d > 0]
6. Imutabilidade com frozen=True e Boas Práticas
Objetos imutáveis são mais seguros em contextos concorrentes e como chaves de dicionários:
@dataclass(frozen=True)
class Ponto:
x: float
y: float
p = Ponto(1.0, 2.0)
# p.x = 3.0 # Erro: cannot assign to field 'x'
# Para modificar em __post_init__:
@dataclass(frozen=True)
class PontoNormalizado:
x: float
y: float
def __post_init__(self):
# Usar object.__setattr__ para contornar frozen
norm = (self.x**2 + self.y**2)**0.5
object.__setattr__(self, 'x', self.x / norm)
object.__setattr__(self, 'y', self.y / norm)
Quando usar o quê?
- Dataclass: Estruturas de dados com lógica adicional (validação, métodos).
- NamedTuple: Imutável, mais leve, ideal para dados simples e desempenho crítico.
- Dicionário: Flexível, mas sem validação de tipos ou autocompletar em IDEs.
7. Integração com Outros Recursos do Python
Propriedades derivadas:
@dataclass
class Retangulo:
largura: float
altura: float
@property
def area(self) -> float:
return self.largura * self.altura
@property
def perimetro(self) -> float:
return 2 * (self.largura + self.altura)
Protocolos e duck typing:
from typing import Protocol
class Serializavel(Protocol):
def to_dict(self) -> dict: ...
@dataclass
class Item:
nome: str
preco: float
def to_dict(self) -> dict:
return {"nome": self.nome, "preco": self.preco}
def exportar(obj: Serializavel):
return obj.to_dict()
Serialização com asdict e astuple:
from dataclasses import asdict, astuple
@dataclass
classe Endereco:
rua: str
numero: int
@dataclass
class Cliente:
nome: str
endereco: Endereco
c = Cliente("Ana", Endereco("Rua A", 123))
print(asdict(c)) # {'nome': 'Ana', 'endereco': {'rua': 'Rua A', 'numero': 123}}
print(astuple(c)) # ('Ana', Endereco(rua='Rua A', numero=123))
asdict é especialmente útil para converter dataclasses aninhadas em dicionários prontos para serialização JSON.
Dataclasses transformaram a maneira como escrevemos classes de dados em Python, reduzindo boilerplate e aumentando a expressividade. Combinadas com type hints, validação pós-inicialização e imutabilidade opcional, são uma ferramenta indispensável no arsenal do desenvolvedor Python moderno.
Referências
- PEP 557 – Data Classes — Proposta oficial que introduziu dataclasses no Python.
- Documentação oficial: dataclasses — Referência completa da API, incluindo todos os parâmetros e funções auxiliares.
- Real Python: Python Data Classes — Tutorial prático com exemplos detalhados de uso no mundo real.
- GeeksforGeeks: Data Classes in Python — Guia introdutório com comparações entre dataclasses e classes tradicionais.
- Towards Data Science: Python Dataclasses — A Comprehensive Guide — Artigo aprofundado sobre configurações avançadas e boas práticas.