Uma dataclass escreve os métodos chatos por você. Você declara os campos com anotações de tipo e o decorator gera __init__, __repr__ e __eq__.
Ela está na biblioteca padrão desde o Python 3.7, então não há nada para instalar.
O código repetitivo que ela elimina
A classe antiga e a dataclass abaixo dela fazem o mesmo trabalho.
# the boilerplate a dataclass removes
class PointOld:
def __init__(self, x, y):
self.x = x
self.y = y
def __repr__(self):
return f'PointOld(x={self.x}, y={self.y})'
def __eq__(self, other):
return isinstance(other, PointOld) and (self.x, self.y) == (other.x, other.y)
from dataclasses import dataclass, field, asdict, replace
@dataclass
class Point:
x: int
y: int
p = Point(1, 2)
print(p)
print(p == Point(1, 2))
Ele imprime:
Point(x=1, y=2)
True
A igualdade compara os campos, não a identidade, que é quase sempre o que você quer num objeto de valor.
Padrões, e a regra das listas
Padrões simples são escritos normalmente. Um padrão mutável precisa de default_factory, que é chamado uma vez por instância.
# defaults, and why lists need default_factory
@dataclass
class Basket:
owner: str
items: list = field(default_factory=list)
currency: str = 'INR'
b1, b2 = Basket('ada'), Basket('grace')
b1.items.append('apple')
print(b1)
print(b2)
try:
@dataclass
class Broken:
items: list = []
except ValueError as err:
print(type(err).__name__ + ':', err)
Ele imprime:
Basket(owner='ada', items=['apple'], currency='INR')
Basket(owner='grace', items=[], currency='INR')
ValueError: mutable default <class 'list'> for field items is not allowed: use default_factory
As últimas linhas são a parte útil. O Python recusa o padrão mutável em vez de deixar todas as instâncias compartilharem uma lista — que é exatamente o que acontece com um argumento comum de função.
Instâncias congeladas
frozen=True torna os campos somente leitura e a instância hasheável, então ela pode ser chave de dicionário ou entrar num set.
# frozen instances are hashable and read only
@dataclass(frozen=True)
class Config:
host: str
port: int = 8080
c = Config('localhost')
print(c, hash(c) == hash(Config('localhost')))
try:
c.port = 9090
except Exception as err:
print(type(err).__name__ + ':', err)
Ele imprime:
Config(host='localhost', port=8080) True
FrozenInstanceError: cannot assign to field 'port'
Ordenação
order=True gera os métodos de comparação, que comparam os campos na ordem em que foram declarados, como uma tupla faria.
# order gives you comparisons and sorting
@dataclass(order=True)
class Version:
major: int
minor: int
versions = [Version(1, 4), Version(1, 2), Version(0, 9)]
print(sorted(versions))
print(Version(1, 4) > Version(1, 2))
Ele imprime:
[Version(major=0, minor=9), Version(major=1, minor=2), Version(major=1, minor=4)]
True
A ordenação funciona sem nenhuma função de chave.
Valores derivados com post_init
field(init=False) mantém um campo fora do construtor, e __post_init__ roda logo depois dele.
# __post_init__ for derived values
@dataclass
class Rect:
width: float
height: float
area: float = field(init=False)
def __post_init__(self):
self.area = self.width * self.height
print(Rect(3, 4))
Ele imprime:
Rect(width=3, height=4, area=12)
asdict e replace
asdict converte para dicionários comuns, recursivamente. replace constrói uma instância nova com alguns campos alterados — que é como você atualiza uma instância congelada.
# asdict and replace
print(asdict(Basket('ada', ['apple'])))
print(replace(Config('localhost'), port=9090))
Ele imprime:
{'owner': 'ada', 'items': ['apple'], 'currency': 'INR'}
Config(host='localhost', port=9090)
Deixando um campo fora do repr ou da igualdade
Útil para segredos, caches e qualquer coisa barulhenta.
# fields you do not want in repr or comparison
@dataclass
class User:
name: str
token: str = field(repr=False, compare=False)
print(User('ada', 'secret-token'))
print(User('ada', 'secret-token') == User('ada', 'different-token'))
Ele imprime:
User(name='ada')
True
O token não é impresso e não afeta a igualdade.
O que lembrar
-
O decorator gera
__init__,__repr__e__eq__a partir dos campos anotados. -
Padrões mutáveis precisam de
field(default_factory=list). -
frozen=Truedá instâncias somente leitura e hasheáveis. -
asdictereplacecobrem serialização e atualização.
Se você também quer validação e parsing nas bordas do seu programa, é aí que o Pydantic ganha o lugar dele. Para simples portadores de dados internos, uma dataclass basta e não custa nada.