Pular para o conteúdo

    Capítulo 36, Intermediário

    dataclasses

    Uma dataclass escreve por você o `__init__`, o `__repr__` e o `__eq__`. Para classes que existem principalmente para guardar dados, elimina um monte de código repetido.

    Código deste capítulo: intermediario/cap36_dataclasses.py

    O básico

    Basta declarar os campos com tipo, e o decorador gera o resto:

    intermediario/cap36_dataclasses.pylinhas 10 a 22
    from dataclasses import dataclass, field, asdict, replace
    
    
    @dataclass
    class Produto:
        nome: str
        preco: float
        tags: list[str] = field(default_factory=list)
    
    
    p = Produto("caneta", 3.5)
    print(p)
    print(p == Produto("caneta", 3.5))
    
    Saída
    Produto(nome='caneta', preco=3.5, tags=[])
    True
    

    O valor padrão mutável, outra vez

    O mesmo problema do capítulo 18 volta aqui, e a dataclass o impede com um erro claro. Para campos mutáveis, use field(default_factory=...):

    intermediario/cap36_dataclasses.pylinhas 27 a 32
    try:
        @dataclass
        class Ruim:
            itens: list = []
    except ValueError as erro:
        print(erro)
    
    Saída
    mutable default <class 'list'> for field itens is not allowed: use default_factory
    

    Imutável, ordenável e econômica

    As opções do decorador cobrem os casos comuns. frozen=True impede a alteração depois de criado (e torna o objeto hashable). order=True gera os operadores de comparação. slots=True reduz a memória de cada instância:

    intermediario/cap36_dataclasses.pylinhas 37 a 48
    @dataclass(frozen=True, order=True, slots=True)
    class Versao:
        major: int
        minor: int = 0
    
    
    v1, v2 = Versao(1, 2), Versao(1, 10)
    print(v1 < v2, sorted([v2, v1]))
    try:
        v1.major = 5
    except Exception as erro:
        print(type(erro).__name__)
    
    Saída
    True [Versao(major=1, minor=2), Versao(major=1, minor=10)]
    FrozenInstanceError
    

    Validação, cópia e conversão

    O método __post_init__ roda logo depois do __init__ e é o lugar da validação. As funções asdict e replace convertem para dicionário e criam uma cópia com campos trocados, sem mutar o original:

    intermediario/cap36_dataclasses.pylinhas 53 a 71
    @dataclass
    class Pedido:
        cliente: str
        itens: list[tuple[str, float]] = field(default_factory=list)
    
        def __post_init__(self):
            if not self.cliente:
                raise ValueError("cliente é obrigatório")
    
        @property
        def total(self):
            return sum(preco for _, preco in self.itens)
    
    
    pedido = Pedido("Ana", [("caneta", 3.5), ("caderno", 18.9)])
    print(round(pedido.total, 2))
    print(asdict(pedido))
    copia = replace(pedido, cliente="Bia")
    print(copia.cliente, pedido.cliente)
    
    Saída
    22.4
    {'cliente': 'Ana', 'itens': [('caneta', 3.5), ('caderno', 18.9)]}
    Bia Ana
    

    Quando usar cada opção

    Preciso de...Uso
    Um registro imutável e leve, que também se comporta como tuplaNamedTuple
    Dados de fora (JSON) com forma conhecida, só para ferramentas de tipoTypedDict
    Um objeto de domínio com dados, métodos e validação simplesdataclass
    Validação e conversão pesadas de dados de entradaUma biblioteca como o Pydantic

    Exercício 1

    Um livro validado

    Crie a dataclass Livro(titulo, paginas) que recuse paginas menor ou igual a zero com ValueError.