Blog

Dataclasses em Python

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=True dá instâncias somente leitura e hasheáveis.

  • asdict e replace cobrem 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.

How useful was this post?

Click on a heart to rate it!

Average rating 0 / 5. Vote count: 0

No votes so far! Be the first to rate this post.