Dataclasses
Esta aula apresenta as dataclasses do Python, um recurso que simplifica a criação de classes para armazenar dados. Aborda a definição com o decorador @dataclass, configuração de campos e valores padrão, uso do parâmetro frozen para imutabilidade, e comparação com namedtuple e dict.
Dataclasses são um recurso introduzido no Python 3.7 através do módulo dataclasses que permitem criar classes de dados de forma concisa e com menos código boilerplate. Elas geram automaticamente métodos como __init__, __repr__, __eq__ e outros, baseados nos campos da classe. Isso torna o código mais limpo e legível, especialmente quando você precisa de uma classe que é principalmente um contêiner de dados.
Nesta aula, vamos explorar como usar dataclasses, configurar campos e valores padrão, torná-las imutáveis com frozen=True, e compará-las com alternativas como namedtuple e dicionários comuns.
@dataclass
O decorador @dataclass é aplicado a uma classe para automaticamente gerar métodos especiais. Para usá-lo, importe dataclass do módulo dataclasses. A classe deve definir atributos com anotações de tipo (type hints). O decorador analisa essas anotações e cria o método __init__ com os parâmetros correspondentes, além de __repr__ e __eq__.
Exemplo básico:
from dataclasses import dataclass
@dataclass
class Pessoa:
nome: str
idade: int
email: str
p = Pessoa("Alice", 30, "alice@example.com")
print(p) # Pessoa(nome='Alice', idade=30, email='alice@example.com')
print(p == Pessoa("Alice", 30, "alice@example.com")) # TrueNote que não precisamos escrever __init__ ou __repr__ manualmente. A ordem dos campos na definição da classe determina a ordem dos argumentos no construtor.
Campos e defaults
Você pode definir valores padrão para campos, inclusive usando a função field() para configurações mais avançadas, como campos que não devem ser incluídos na representação ou na comparação. Valores padrão são atribuídos diretamente na anotação.
Exemplo com valores padrão:
from dataclasses import dataclass
@dataclass
class Config:
host: str = "localhost"
port: int = 8080
debug: bool = False
c = Config()
print(c) # Config(host='localhost', port=8080, debug=False)
c2 = Config("0.0.0.0", 80, True)
print(c2) # Config(host='0.0.0.0', port=80, debug=True)A função field() permite especificar opções como default, default_factory (para tipos mutáveis como listas), repr, compare, hash e init. Exemplo:
from dataclasses import dataclass, field
from typing import List
@dataclass
class Aluno:
nome: str
notas: List[float] = field(default_factory=list)
ativo: bool = True
a = Aluno("João")
a.notas.append(8.5)
print(a) # Aluno(nome='João', notas=[8.5], ativo=True)Se usássemos notas: List[float] = [], o valor padrão seria compartilhado entre todas as instâncias, causando bugs. default_factory resolve isso criando uma nova lista para cada instância.
frozen
O parâmetro frozen=True no decorador @dataclass torna a classe imutável: após a criação, não é possível alterar os atributos. Isso é útil para criar objetos que devem ser constantes ou seguros em contextos de hash. Dataclasses imutáveis também podem ser usadas como chaves de dicionários.
Exemplo:
from dataclasses import dataclass
@dataclass(frozen=True)
class Ponto:
x: float
y: float
p = Ponto(1.0, 2.0)
print(p.x) # 1.0
# p.x = 3.0 # Isso lançaria FrozenInstanceError
d = {p: "origem"} # Funciona, pois Ponto é hashable
print(d) # {Ponto(x=1.0, y=2.0): 'origem'}Com frozen=True, a dataclass automaticamente gera métodos __hash__ (se eq=True, que é o padrão). Isso permite que instâncias sejam usadas em conjuntos e como chaves de dicionário.
vs namedtuple e dict
Antes das dataclasses, era comum usar namedtuple (do módulo collections) ou dicionários para representar dados simples. Vamos comparar as abordagens.
namedtuple: Cria uma tupla com campos nomeados. É imutável e mais leve que uma classe, mas não permite métodos personalizados, valores padrão ou type hints de forma nativa (embora Python 3.6+ permita anotações). Exemplo:
from collections import namedtuple
Ponto = namedtuple("Ponto", ["x", "y"])
p = Ponto(1, 2)
print(p.x, p.y) # 1 2
# p.x = 3 # Erro: tuplas são imutáveisdict: Dicionários são flexíveis e dinâmicos, mas não fornecem acesso a atributos com ponto, validação de tipos ou métodos especiais. Exemplo:
p = {"x": 1, "y": 2}
print(p["x"]) # 1
p["x"] = 3 # PermitidoDataclass: Oferece o melhor dos dois mundos: mutabilidade opcional, type hints, valores padrão, métodos automáticos e possibilidade de adicionar métodos personalizados. Comparado a namedtuple, as dataclasses são mais flexíveis e legíveis para classes complexas. Comparado a dict, fornecem uma interface mais segura e autodocumentada.
Tabela comparativa:
- namedtuple: Imutável, leve, sem type hints nativos, sem valores padrão, sem métodos personalizados.
- dict: Mutável, dinâmico, sem estrutura fixa, sem acesso por atributo, sem validação.
- dataclass: Mutável (ou imutável com frozen), type hints, valores padrão, métodos automáticos, suporte a herança.
Em geral, prefira dataclasses para representar dados estruturados em seu código, a menos que precise de imutabilidade simples (use namedtuple) ou flexibilidade total (use dict).
Boas práticas
- Sempre use type hints nos campos da dataclass; isso melhora a legibilidade e permite que ferramentas de análise estática verifiquem seu código.
- Use
field(default_factory=...)para valores padrão mutáveis, evitando o problema de estado compartilhado. - Para classes que representam entidades imutáveis (como coordenadas), use
frozen=Truee aproveite a hashability. - Não abuse de dataclasses para classes com comportamento complexo; elas são projetadas para armazenar dados, não para lógica de negócio pesada.
Referências
- Documentação oficial do módulo dataclasses
- Documentação de namedtuple
- Real Python: Data Classes in Python
- PEP 557 – Data Classes
- Documentação de dicionários
- Mypy: Dataclasses
Exercícios
- Crie uma dataclass chamada
Livrocom campos:titulo(str),autor(str),ano(int) epreco(float). Instancie um livro e imprima sua representação. - Defina uma dataclass
Configuracaocom camposhost(str) com valor padrão "localhost",porta(int) com valor padrão 8080, essl(bool) com valor padrão False. Crie uma instância sem argumentos e outra com argumentos personalizados. - Crie uma dataclass
Alunocom camposnome(str),matricula(int) enotas(List[float]) usandofield(default_factory=list). Adicione um método que calcule a média das notas. Instancie um aluno, adicione notas e exiba a média. - Crie uma dataclass imutável
Ponto2Dcom camposxey(float). Verifique se a instância é hashable e use-a como chave em um dicionário. - Converta a seguinte namedtuple para uma dataclass equivalente e adicione um campo
emailcom valor padrão vazio:Usuario = namedtuple('Usuario', ['nome', 'idade']).
from dataclasses import dataclass
@dataclass
class Livro:
titulo: str
autor: str
ano: int
preco: float
livro = Livro("1984", "George Orwell", 1949, 29.90)
print(livro) # Livro(titulo='1984', autor='George Orwell', ano=1949, preco=29.9)
from dataclasses import dataclass
@dataclass
class Configuracao:
host: str = "localhost"
porta: int = 8080
ssl: bool = False
c1 = Configuracao()
print(c1) # Configuracao(host='localhost', porta=8080, ssl=False)
c2 = Configuracao("example.com", 443, True)
print(c2) # Configuracao(host='example.com', porta=443, ssl=True)
from dataclasses import dataclass, field
from typing import List
@dataclass
class Aluno:
nome: str
matricula: int
notas: List[float] = field(default_factory=list)
def media(self) -> float:
if not self.notas:
return 0.0
return sum(self.notas) / len(self.notas)
aluno = Aluno("Maria", 12345)
aluno.notas.extend([7.5, 8.0, 9.2])
print(aluno.media()) # 8.2333...
from dataclasses import dataclass
@dataclass(frozen=True)
class Ponto2D:
x: float
y: float
p1 = Ponto2D(1.0, 2.0)
p2 = Ponto2D(3.0, 4.0)
print(hash(p1)) # Valor hash
coordenadas = {p1: "origem", p2: "destino"}
print(coordenadas) # {Ponto2D(x=1.0, y=2.0): 'origem', Ponto2D(x=3.0, y=4.0): 'destino'}
from dataclasses import dataclass
@dataclass
class Usuario:
nome: str
idade: int
email: str = ""
u = Usuario("João", 25)
print(u) # Usuario(nome='João', idade=25, email='')
u2 = Usuario("Maria", 30, "maria@example.com")
print(u2) # Usuario(nome='Maria', idade=30, email='maria@example.com')