Python Dataclasses
Aprenda Python dataclasses: o decorador @dataclass, defaults com field(), ordenação, imutabilidade e herança com exemplos práticos.
Uma dataclass é uma classe Python comum cujo código repetitivo — __init__, __repr__ e __eq__ — é gerado automaticamente pelo decorador @dataclass. O resultado é menos código, menos erros de digitação e classes imediatamente legíveis.
Este capítulo cobre:
- Por que dataclasses existem e quando usá-las
- O decorador
@dataclass - Defaults de campos e o auxiliar
field() - Controle de igualdade e ordenação
- Dataclasses imutáveis com
frozen=True - Lógica pós-inicialização com
__post_init__ - Herança com dataclasses
- Dataclasses vs.
NamedTuplevs. classes simples
Antes de ler este capítulo, certifique-se de estar familiarizado com classes e objetos Python e herança Python.
Por que Dataclasses?
Considere uma classe que armazena um produto em uma loja online. Sem dataclasses, você escreve as mesmas atribuições de atributo três vezes — uma vez em __init__, uma vez em __repr__ e uma vez em __eq__:
class Product:
def __init__(self, name, price, stock):
self.name = name
self.price = price
self.stock = stock
def __repr__(self):
return f"Product(name={self.name!r}, price={self.price}, stock={self.stock})"
def __eq__(self, other):
if not isinstance(other, Product):
return NotImplemented
return (self.name, self.price, self.stock) == (other.name, other.price, other.stock)O decorador @dataclass gera tudo o acima a partir de uma única lista anotada de campos:
from dataclasses import dataclass
@dataclass
class Product:
name: str
price: float
stock: intAmbas as versões se comportam de forma idêntica. A versão com dataclass é mais curta, mais difícil de errar e comunica imediatamente que essa classe é principalmente um contêiner de dados.
O Decorador @dataclass
Importe dataclass do módulo da biblioteca padrão dataclasses e aplique-o à sua classe. Cada campo é declarado como uma variável de classe com anotação de tipo:
from dataclasses import dataclass
@dataclass
class Point:
x: float
y: float
p = Point(1.5, 2.0)
print(p) # Point(x=1.5, y=2.0)
print(p.x) # 1.5
p2 = Point(1.5, 2.0)
print(p == p2) # True — __eq__ compares field by fieldO decorador gera:
| Método | O que faz |
|---|---|
__init__ | Aceita cada campo como parâmetro e o atribui a self |
__repr__ | Retorna uma string legível como Point(x=1.5, y=2.0) |
__eq__ | Compara duas instâncias campo a campo |
Anotações de tipo são obrigatórias, mas não verificadas em tempo de execução
Declarações de campo exigem uma anotação de tipo (x: float). Python não verifica o tipo em tempo de execução — você ainda pode passar uma string onde um float é esperado. A anotação é metadado usado por verificadores de tipo como mypy e pela própria maquinaria do dataclasses. Para validação de tipo em tempo de execução, consulte Python Type Hints.
Valores Padrão
Atribua um valor padrão diretamente no campo para torná-lo opcional em __init__:
from dataclasses import dataclass
@dataclass
class Config:
host: str = "localhost"
port: int = 8080
debug: bool = False
c1 = Config()
print(c1) # Config(host='localhost', port=8080, debug=False)
c2 = Config(host="example.com", port=443)
print(c2) # Config(host='example.com', port=443, debug=False)Campos com padrões devem aparecer depois de campos sem padrões — exatamente a mesma regra que se aplica a parâmetros de funções regulares.
Padrões mutáveis e field()
Não é possível usar um objeto mutável (uma lista, dict ou set) como valor padrão simples. Python compartilharia uma lista entre todas as instâncias, o que leva a bugs sutis:
from dataclasses import dataclass
# This raises a ValueError at class definition time:
# @dataclass
# class Bag:
# items: list = [] # ValueError: mutable default is not allowedEm vez disso, use field(default_factory=...) para criar um novo objeto para cada instância:
from dataclasses import dataclass, field
@dataclass
class Bag:
items: list = field(default_factory=list)
b1 = Bag()
b2 = Bag()
b1.items.append("apple")
print(b1.items) # ['apple']
print(b2.items) # [] — b2 has its own separate listdefault_factory aceita qualquer chamável sem argumentos, incluindo lambdas e suas próprias funções.
O Auxiliar field()
field() oferece controle detalhado sobre campos individuais. Seus parâmetros mais úteis são:
| Parâmetro | Finalidade |
|---|---|
default | Um valor padrão simples (somente escalar) |
default_factory | Um chamável que produz o padrão |
repr | False para excluir este campo de __repr__ |
compare | False para excluir este campo de __eq__ (e ordenação) |
init | False para excluir este campo de __init__ |
from dataclasses import dataclass, field
import time
@dataclass
class LogEntry:
message: str
level: str = "INFO"
timestamp: float = field(default_factory=time.time, repr=False, compare=False)
entry = LogEntry("Server started")
print(entry) # LogEntry(message='Server started', level='INFO')
# timestamp exists but is hidden from repr and ignored in comparisons
print(entry.timestamp > 0) # TrueOrdenação
Por padrão, dataclasses suportam igualdade (==, !=) mas não ordenação (<, >, <=, >=). Habilite a ordenação passando order=True ao decorador:
from dataclasses import dataclass
@dataclass(order=True)
class Version:
major: int
minor: int
patch: int
v1 = Version(1, 2, 0)
v2 = Version(1, 3, 0)
v3 = Version(1, 2, 0)
print(v1 < v2) # True
print(v1 == v3) # True
print(v2 > v1) # True
versions = [Version(2, 0, 0), Version(1, 9, 1), Version(1, 2, 3)]
print(sorted(versions))
# [Version(major=1, minor=2, patch=3),
# Version(major=1, minor=9, patch=1),
# Version(major=2, minor=0, patch=0)]Python gera os métodos de comparação comparando campos na ordem em que são declarados, no estilo de tupla. Você pode excluir um campo das comparações com field(compare=False).
Dataclasses Imutáveis com frozen=True
Passe frozen=True para tornar todos os campos somente leitura após a criação. Qualquer tentativa de alterar um campo gera um FrozenInstanceError:
from dataclasses import dataclass
@dataclass(frozen=True)
class Coordinate:
lat: float
lon: float
london = Coordinate(51.5074, -0.1278)
print(london) # Coordinate(lat=51.5074, lon=-0.1278)
# london.lat = 0.0 # FrozenInstanceError: cannot assign to field 'lat'Dataclasses congeladas também são hasheáveis (implementam __hash__), portanto você pode usá-las como chaves de dicionário ou membros de set:
from dataclasses import dataclass
@dataclass(frozen=True)
class Coordinate:
lat: float
lon: float
cities = {
Coordinate(51.5074, -0.1278): "London",
Coordinate(48.8566, 2.3522): "Paris",
}
print(cities[Coordinate(51.5074, -0.1278)]) # LondonDataclasses regulares (mutáveis) não são hasheáveis por padrão — Python define __hash__ como None quando __eq__ é definido sem frozen=True.
Lógica Pós-Inicialização com __post_init__
Às vezes você precisa derivar o valor de um campo a partir de outros campos, ou validar a entrada após a execução de __init__. Defina um método __post_init__ — ele é chamado automaticamente ao final do __init__ gerado:
from dataclasses import dataclass, field
import math
@dataclass
class Circle:
radius: float
def __post_init__(self):
if self.radius <= 0:
raise ValueError(f"radius must be positive, got {self.radius}")
@property
def area(self):
return math.pi * self.radius ** 2
c = Circle(5)
print(round(c.area, 4)) # 78.5398
# Circle(-1) # ValueError: radius must be positive, got -1Você também pode calcular um campo derivado. Marque-o com field(init=False) para que não apareça em __init__, e então defina-o dentro de __post_init__:
from dataclasses import dataclass, field
@dataclass
class Rectangle:
width: float
height: float
area: float = field(init=False, repr=True)
def __post_init__(self):
self.area = self.width * self.height
r = Rectangle(4, 6)
print(r) # Rectangle(width=4, height=6, area=24)
print(r.area) # 24Herança com Dataclasses
Uma dataclass pode herdar de outra dataclass. O __init__ da classe filha inclui campos de ambas as classes — campos do pai primeiro, na ordem em que foram declarados:
from dataclasses import dataclass
@dataclass
class Animal:
name: str
age: int
@dataclass
class Dog(Animal):
breed: str
rex = Dog(name="Rex", age=3, breed="Labrador")
print(rex) # Dog(name='Rex', age=3, breed='Labrador')Atenção: se uma classe pai tem um campo com padrão, todos os campos da classe filha também devem ter padrões. Esta é a mesma regra que se aplica a assinaturas de funções Python regulares — um parâmetro sem padrão não pode seguir um com padrão.
from dataclasses import dataclass
@dataclass
class Animal:
name: str
age: int = 0 # has a default
# @dataclass
# class Dog(Animal):
# breed: str # TypeError: non-default argument 'breed' follows default argumentContorne isso dando ao campo da classe filha um padrão também, ou reestruturando a hierarquia para que campos com padrão venham por último.
Parâmetros do Decorador em Resumo
@dataclass(
init=True, # generate __init__ (default True)
repr=True, # generate __repr__ (default True)
eq=True, # generate __eq__ (default True)
order=False, # generate <, >, <=, >= (default False)
frozen=False, # make fields immutable (default False)
)
class MyClass:
...Raramente você precisa alterar a maioria destes. Os mais comuns são order=True e frozen=True.
Funções Utilitárias
O módulo dataclasses também fornece três funções úteis:
fields()
Retorna uma tupla de objetos Field descrevendo cada campo da classe:
from dataclasses import dataclass, fields
@dataclass
class Point:
x: float
y: float
for f in fields(Point):
print(f.name, f.type)
# x <class 'float'>
# y <class 'float'>asdict()
Converte uma instância de dataclass em um dicionário simples (recursivamente):
from dataclasses import dataclass, asdict
@dataclass
class Address:
street: str
city: str
@dataclass
class Person:
name: str
address: Address
p = Person("Alice", Address("10 Downing St", "London"))
print(asdict(p))
# {'name': 'Alice', 'address': {'street': '10 Downing St', 'city': 'London'}}Isso é útil ao serializar para JSON ou enviar dados para uma API.
astuple()
Converte para uma tupla (recursivamente):
from dataclasses import dataclass, astuple
@dataclass
class Point:
x: float
y: float
p = Point(3.0, 4.0)
print(astuple(p)) # (3.0, 4.0)Dataclasses vs. NamedTuple vs. Classes Simples
| Recurso | Classe simples | NamedTuple | dataclass |
|---|---|---|---|
__init__ automático | Não | Sim | Sim |
__repr__ automático | Não | Sim | Sim |
__eq__ automático | Não | Sim (por valor) | Sim (por valor) |
| Mutável | Sim | Não | Sim (padrão) |
| Hasheável | Não (se __eq__ definido) | Sim | Somente com frozen=True |
| Ordenação | Manual | Sim | order=True |
| Herança | Sim | Limitada | Sim |
Verificação isinstance | Sim | Sim (também tuple) | Sim |
Desempacotamento (a, b = obj) | Não | Sim | Não |
Use uma dataclass quando:
- Você quer dados mutáveis com imutabilidade opcional.
- Você precisa de herança ou lógica pós-init.
- Você quer controle detalhado de campos (
field()).
Use NamedTuple quando:
- Você quer um registro imutável que também se comporta como tupla (desempacotamento posicional, linhas CSV).
- Você precisa de compatibilidade com código que espera tuplas.
Use uma classe simples quando:
- A classe tem comportamento significativo e poucos dados simples.
- Você precisa de um
__init__personalizado que não pode ser expresso através de__post_init__.
Armadilhas Comuns
Padrões mutáveis. Usar uma lista ou dict como valor padrão simples gera ValueError no momento da definição da classe. Sempre use field(default_factory=...).
Hashing. Dataclasses regulares não são hasheáveis. Se você precisar usá-las como chaves de dict ou em sets, use frozen=True ou passe unsafe_hash=True (raramente recomendado).
eq=False. Se você desabilitar a geração de igualdade (eq=False), Python volta à comparação por identidade (is), o que quase nunca é o desejado para objetos de dados.
Ordenação de padrões herdados. Se um campo do pai tem um padrão e um campo do filho não tem, Python gera um TypeError. Planeje cuidadosamente a ordenação dos campos na sua hierarquia.
Resumo
| Conceito | O que faz |
|---|---|
@dataclass | Gera __init__, __repr__, __eq__ automaticamente |
field() | Controle detalhado de campos: padrões, repr, compare, init |
default_factory | Fornece um padrão mutável fresco para cada instância |
order=True | Adiciona <, >, <=, >= com base na ordem dos campos |
frozen=True | Torna os campos somente leitura e a instância hasheável |
__post_init__ | Executa após __init__ para validação ou campos derivados |
fields() | Retorna metadados sobre cada campo |
asdict() | Converte instância em um dict simples (recursivamente) |
astuple() | Converte instância em uma tupla simples (recursivamente) |
Para tópicos relacionados, consulte classes e objetos Python, herança Python e classes base abstratas Python.