Python & AI Tutorials Logo
Programação Python

33. Data Classes para Dados Estruturados Simples

No Capítulo 30, aprendemos como criar classes para definir nossos próprios tipos. Nós escrevemos métodos __init__ para inicializar instâncias, métodos __repr__ para exibi-las, e métodos __eq__ para compará-las. Embora essa abordagem funcione perfeitamente, ela envolve escrever muito código repetitivo, especialmente quando uma classe existe principalmente para armazenar dados.

As data classes do Python oferecem uma forma mais limpa e concisa de criar classes que são principalmente contêineres de dados. Ao usar o decorator @dataclass, o Python gera automaticamente métodos comuns como __init__, __repr__ e __eq__ com base nos atributos da classe que você define. Isso reduz o boilerplate e deixa suas intenções mais claras.

33.1) O que são Data Classes e quando usá-las

Uma data class (classe de dados) é uma classe projetada principalmente para armazenar valores de dados. Em vez de escrever manualmente métodos de inicialização e comparação, você define os atributos que sua classe deve ter, e o Python gera automaticamente os métodos necessários.

Por que Data Classes importam

Considere uma classe comum para representar um livro:

python
class Book:
    def __init__(self, title, author, year):
        self.title = title
        self.author = author
        self.year = year
    
    def __repr__(self):
        return f"Book(title={self.title!r}, author={self.author!r}, year={self.year})"
    
    def __eq__(self, other):
        if not isinstance(other, Book):
            return False
        return (self.title == other.title and 
                self.author == other.author and 
                self.year == other.year)
 
book1 = Book("1984", "George Orwell", 1949)
print(book1)  # Output: Book(title='1984', author='George Orwell', year=1949)
 
book2 = Book("1984", "George Orwell", 1949)
print(book1 == book2)  # Output: True

Isso funciona, mas repare quanto código nós escrevemos só para armazenar três informações. Os métodos __init__, __repr__ e __eq__ seguem padrões previsíveis — eles simplesmente lidam com os atributos que definimos.

As data classes eliminam essa repetição. Elas são particularmente úteis quando:

  • Sua classe armazena principalmente dados em vez de implementar comportamentos complexos
  • Você precisa de métodos padrão como inicialização, representação em string e comparação de igualdade
  • Você quer um código mais claro e mais fácil de manter com menos boilerplate
  • Você está criando objetos de configuração, objetos de transferência de dados ou registros simples

Data classes não substituem classes comuns — elas as complementam. Use classes comuns quando você precisa de lógica de inicialização personalizada, métodos complexos ou hierarquias de herança. Use data classes quando você precisa principalmente de um contêiner estruturado para dados relacionados.

A relação entre Data Classes e classes comuns

Data classes ainda são classes Python comuns. Elas suportam todos os recursos que aprendemos nos Capítulos 30-32: métodos, propriedades, herança e métodos especiais. O decorator @dataclass simplesmente automatiza a criação de métodos comuns, poupando você de escrever código repetitivo.

Classe Comum

init Manual

repr Manual

eq Manual

Métodos Personalizados

Data Class

Decorator @dataclass

init Gerado Automaticamente

repr Gerado Automaticamente

eq Gerado Automaticamente

Métodos Personalizados

33.2) Criando Data Classes com @dataclass

Para criar uma data class, você importa o decorator dataclass do módulo dataclasses e aplica ele à definição da sua classe. Dentro da classe, você define atributos de classe com anotações de tipo que especificam quais dados a classe deve manter.

Sintaxe básica de Data Class

python
from dataclasses import dataclass
 
@dataclass
class Student:
    name: str
    student_id: int
    gpa: float
 
# Criar instâncias
alice = Student("Alice Johnson", 12345, 3.8)
bob = Student("Bob Smith", 12346, 3.5)
 
print(alice)  # Output: Student(name='Alice Johnson', student_id=12345, gpa=3.8)
print(bob)    # Output: Student(name='Bob Smith', student_id=12346, gpa=3.5)

Vamos detalhar o que o decorator @dataclass faz:

  1. @dataclass: aplicar esse decorator faz com que o Python escreva automaticamente os métodos __init__, __repr__ e __eq__ para você

  2. __init__ automático: o Python cria um método de inicialização que aceita esses três parâmetros na ordem em que eles são definidos e os atribui a atributos de instância

  3. __repr__ automático: o Python cria uma representação em string mostrando o nome da classe e todos os valores dos atributos

  4. __eq__ automático: o Python cria um método de comparação de igualdade que compara todos os atributos

  5. Converte anotações de tipo em atributos de instância: em uma classe comum, escrever name: str no corpo da classe cria um atributo de classe. Mas o decorator @dataclass muda esse comportamento — ele usa essas anotações de tipo para definir atributos de instância. Cada instância recebe seus próprios atributos name, student_id e gpa.

A principal diferença em relação às classes comuns:

python
# Classe comum - estes são atributos de classe (compartilhados por todas as instâncias)
class RegularStudent:
    name: str
    student_id: int
 
# Data class - estes viram atributos de instância (cada instância tem os seus)
@dataclass
class DataStudent:
    name: str
    student_id: int

Entendendo anotações de tipo em Data Classes

Em data classes, as anotações de tipo definem os atributos e documentam seus tipos esperados:

python
from dataclasses import dataclass
 
@dataclass
class Product:
    name: str
    price: float
    in_stock: bool
 
# Usando os tipos corretos conforme documentado
laptop = Product("Laptop", 999.99, True)
print(laptop)  # Output: Product(name='Laptop', price=999.99, in_stock=True)
 
# O Python não impõe tipos - isso roda sem erro
macbook = Product("Macbook", "expensive", True)
print(macbook)  # Output: Product(name='Macbook', price='expensive', in_stock=True)
 
# Mas usar tipos errados vai causar problemas depois:
discounted = laptop.price * 0.9     # Works: 899.991
discounted = macbook.price * 0.9    # TypeError: can't multiply sequence by non-int of type 'float'
 
tax = laptop.price + 50             # Works: 1049.99
tax = macbook.price + 50            # TypeError: can only concatenate str (not "int") to str

O Python não vai impedir você de passar os tipos errados ao criar uma instância de data class. As anotações de tipo são principalmente documentação — elas dizem a outros programadores (e a ferramentas de verificação de tipos como o mypy) quais tipos você espera, mas o Python não as impõe em runtime. Isso é consistente com a filosofia de tipagem dinâmica do Python.

No entanto, seguir as anotações de tipo torna seu código mais previsível e mais fácil de debugar. Quando você usa os tipos errados, erros vão aparecer depois, quando você tentar usar os dados, tornando os bugs mais difíceis de rastrear. Ferramentas de verificação de tipos podem detectar essas incompatibilidades antes de você rodar o código, ajudando você a encontrar problemas cedo.

Acessando e modificando atributos

Instâncias de data class funcionam exatamente como instâncias de classes comuns. Você acessa e modifica atributos usando notação de ponto:

python
from dataclasses import dataclass
 
@dataclass
class Employee:
    name: str
    position: str
    salary: float
 
emp = Employee("Sarah Chen", "Software Engineer", 95000.0)
 
# Acessar atributos
print(emp.name)      # Output: Sarah Chen
print(emp.position)  # Output: Software Engineer
 
# Modificar atributos
emp.salary = 100000.0
emp.position = "Senior Software Engineer"
 
print(emp)  # Output: Employee(name='Sarah Chen', position='Senior Software Engineer', salary=100000.0)

Data classes são mutáveis por padrão — você pode alterar seus atributos após a criação. Isso é diferente de tuplas ou named tuples, que são imutáveis. Se você precisa de imutabilidade, você pode configurar a data class com frozen=True (vamos explorar isso na Seção 33.4).

33.3) Métodos gerados: __init__, __repr__ e __eq__

O decorator @dataclass gera automaticamente três métodos essenciais. Entender o que esses métodos fazem ajuda você a usar data classes de forma eficaz e saber quando personalizá-las.

O método __init__ gerado

O método __init__ inicializa uma nova instância com os valores fornecidos. O Python o gera com base na ordem das suas definições de atributos:

python
from dataclasses import dataclass
 
@dataclass
class Rectangle:
    width: float
    height: float
 
# O __init__ gerado aceita width e height nessa ordem
rect = Rectangle(10.5, 5.0)
print(rect.width)   # Output: 10.5
print(rect.height)  # Output: 5.0
 
# Você também pode usar argumentos nomeados
rect2 = Rectangle(height=8.0, width=12.0)
print(rect2.width)   # Output: 12.0
print(rect2.height)  # Output: 8.0

O __init__ gerado é equivalente a escrever:

python
def __init__(self, width: float, height: float):
    self.width = width
    self.height = height

Essa geração automática poupa você de escrever código repetitivo de inicialização, especialmente para classes com muitos atributos.

O método __repr__ gerado

O método __repr__ fornece uma representação em string da instância que mostra todos os valores dos atributos. Isso é inestimável para debugging e logging:

python
from dataclasses import dataclass
 
@dataclass
class Point:
    x: float
    y: float
    label: str
 
point = Point(3.5, 7.2, "A")
print(point)  # Output: Point(x=3.5, y=7.2, label='A')
print(repr(point))  # Output: Point(x=3.5, y=7.2, label='A')

O __repr__ gerado segue a convenção de mostrar o nome da classe e todos os atributos em um formato que poderia ser usado para recriar o objeto. Isso é muito mais útil do que a representação padrão que você teria sem __repr__: <__main__.Point object at 0x...>.

O método __eq__ gerado

O método __eq__ permite comparação de igualdade entre instâncias. Duas instâncias de data class são consideradas iguais se todos os seus atributos correspondentes forem iguais:

python
from dataclasses import dataclass
 
@dataclass
class Color:
    red: int
    green: int
    blue: int
 
color1 = Color(255, 0, 0)
color2 = Color(255, 0, 0)
color3 = Color(0, 255, 0)
 
print(color1 == color2)  # Output: True (same RGB values)
print(color1 == color3)  # Output: False (different RGB values)
print(color1 is color2)  # Output: False (different objects in memory)

Essa comparação automática de igualdade é baseada em igualdade de valor, não de identidade. Mesmo que color1 e color2 sejam objetos diferentes na memória (como mostrado por is), eles são considerados iguais porque seus atributos batem.

O método __eq__ gerado compara atributos na ordem em que eles são definidos:

python
from dataclasses import dataclass
 
@dataclass
class Book:
    title: str
    author: str
    year: int
 
book1 = Book("1984", "George Orwell", 1949)
book2 = Book("1984", "George Orwell", 1949)
book3 = Book("Animal Farm", "George Orwell", 1945)
 
print(book1 == book2)  # Output: True (all attributes match)
print(book1 == book3)  # Output: False (title and year differ)
 
# Comparação com objetos que não são Book retorna False
print(book1 == "1984")  # Output: False
print(book1 == None)    # Output: False

Comparando métodos gerados com implementação manual

Para valorizar o que as data classes oferecem, vamos comparar a versão com data class com uma implementação manual:

python
from dataclasses import dataclass
 
# Versão data class (concisa)
@dataclass
class PersonData:
    first_name: str
    last_name: str
    age: int
 
# Versão manual equivalente (verbosa)
class PersonManual:
    def __init__(self, first_name: str, last_name: str, age: int):
        self.first_name = first_name
        self.last_name = last_name
        self.age = age
    
    def __repr__(self):
        return f"PersonManual(first_name={self.first_name!r}, last_name={self.last_name!r}, age={self.age})"
    
    def __eq__(self, other):
        if not isinstance(other, PersonManual):
            return False
        return (self.first_name == other.first_name and
                self.last_name == other.last_name and
                self.age == other.age)
 
# Ambas funcionam de forma idêntica
p1 = PersonData("Alice", "Johnson", 30)
p2 = PersonManual("Alice", "Johnson", 30)
 
print(p1)  # Output: PersonData(first_name='Alice', last_name='Johnson', age=30)
print(p2)  # Output: PersonManual(first_name='Alice', last_name='Johnson', age=30)

A versão com data class alcança a mesma funcionalidade com bem menos código. Essa redução de boilerplate torna seu código mais fácil de ler, manter e modificar.

Adicionando métodos personalizados a Data Classes

Data classes podem ter métodos personalizados assim como classes comuns. O decorator @dataclass só gera os métodos de inicialização, representação e igualdade — você fica livre para adicionar qualquer outra funcionalidade:

python
from dataclasses import dataclass
 
@dataclass
class Temperature:
    celsius: float
    
    def to_fahrenheit(self):
        """Convert temperature to Fahrenheit."""
        return (self.celsius * 9/5) + 32
    
    def to_kelvin(self):
        """Convert temperature to Kelvin."""
        return self.celsius + 273.15
    
    def is_freezing(self):
        """Check if temperature is at or below freezing point."""
        return self.celsius <= 0
 
temp = Temperature(25.0)
print(temp)  # Output: Temperature(celsius=25.0)
print(f"{temp.celsius}°C = {temp.to_fahrenheit()}°F")  # Output: 25.0°C = 77.0°F
print(f"Kelvin: {temp.to_kelvin()}")  # Output: Kelvin: 298.15
print(f"Freezing: {temp.is_freezing()}")  # Output: Freezing: False
 
cold_temp = Temperature(-5.0)
print(f"Freezing: {cold_temp.is_freezing()}")  # Output: Freezing: True

Data classes cuidam das partes repetitivas (inicialização, representação e comparação) enquanto deixam você adicionar métodos personalizados para suas necessidades específicas, como mostrado com os métodos de conversão de temperatura acima.

33.4) Valores padrão e opções de campo

Data classes suportam valores padrão para atributos, permitindo que você crie instâncias sem especificar cada parâmetro. Você também pode usar a função field() para configurar comportamentos avançados como excluir atributos de comparações ou controlar como eles aparecem na representação em string.

Fornecendo valores padrão

Você pode atribuir valores padrão aos atributos diretamente na definição da classe. Atributos com padrão precisam vir depois de atributos sem padrão:

python
from dataclasses import dataclass
 
@dataclass
class User:
    username: str
    email: str
    is_active: bool = True  # Valor padrão
    role: str = "user"      # Valor padrão
 
# Criar instâncias com e sem valores padrão
user1 = User("alice", "alice@example.com")
print(user1)  # Output: User(username='alice', email='alice@example.com', is_active=True, role='user')
 
user2 = User("bob", "bob@example.com", False, "admin")
print(user2)  # Output: User(username='bob', email='bob@example.com', is_active=False, role='admin')
 
# Use keyword arguments to override specific defaults
user3 = User("charlie", "charlie@example.com", role="moderator")
print(user3)  # Output: User(username='charlie', email='charlie@example.com', is_active=True, role='moderator')

A regra de ordenação (atributos sem padrão antes de atributos com padrão) evita ambiguidade no método __init__ gerado. Essa é a mesma exigência para parâmetros de função com valores padrão, que aprendemos no Capítulo 20.

Valores padrão mutáveis e por que eles não são permitidos

Data classes protegem você de um erro comum com padrões mutáveis. Se você tentar usar diretamente um objeto mutável como uma lista ou um dicionário como padrão, você vai receber um erro:

python
from dataclasses import dataclass
 
# Isso vai levantar um erro
@dataclass
class ShoppingCart:
    customer: str
    items: list = []  # ValueError: mutable default <class 'list'> for field items is not allowed: use default_factory

Esse erro evita o mesmo problema que vimos com argumentos padrão de funções no Capítulo 20, em que todas as instâncias compartilhariam o mesmo objeto mutável.

Usando field() com default_factory para padrões mutáveis

A solução é usar a função field() com default_factory, que cria um novo valor padrão para cada instância:

python
from dataclasses import dataclass, field
 
@dataclass
class ShoppingCart:
    customer: str
    items: list = field(default_factory=list)  # Correto: nova lista por instância
 
# Agora cada instância recebe sua própria lista
cart1 = ShoppingCart("Alice")
cart1.items.append("Book")
print(cart1.items)  # Output: ['Book']
 
cart2 = ShoppingCart("Bob")
print(cart2.items)  # Output: [] - Bob tem uma lista vazia
 
cart2.items.append("Laptop")
print(cart1.items)  # Output: ['Book'] - Alice's cart unchanged
print(cart2.items)  # Output: ['Laptop'] - Bob's cart independent

O parâmetro default_factory recebe uma função (como list, dict ou set) que será chamada para criar um novo valor padrão toda vez que você criar uma instância sem fornecer esse atributo. Por exemplo, default_factory=list significa que o Python vai chamar list() para criar uma nova lista vazia para cada instância.

Excluindo campos da comparação

Às vezes você quer que certos atributos sejam excluídos de comparações de igualdade. Use field(compare=False) para isso:

python
from dataclasses import dataclass, field
from datetime import datetime
 
@dataclass
class LogEntry:
    message: str
    level: str
    timestamp: datetime = field(compare=False)  # Não comparar timestamps
 
# Criar duas entradas de log com a mesma mensagem, mas horários diferentes
entry1 = LogEntry("User logged in", "INFO", datetime(2024, 1, 15, 10, 30))
entry2 = LogEntry("User logged in", "INFO", datetime(2024, 1, 15, 10, 35))
 
# Elas são iguais porque timestamp é excluído da comparação
print(entry1 == entry2)  # Output: True
 
# Mas elas têm timestamps diferentes
print(entry1.timestamp)  # Output: 2024-01-15 10:30:00
print(entry2.timestamp)  # Output: 2024-01-15 10:35:00

Isso é útil quando você tem campos de metadados (como timestamps, IDs ou contadores internos) que não devem afetar se duas instâncias são consideradas iguais.

Excluindo campos da representação

Você também pode excluir campos da representação em string usando field(repr=False):

python
from dataclasses import dataclass, field
 
@dataclass
class Account:
    username: str
    email: str
    password: str = field(repr=False)  # Não mostrar password no repr
 
account = Account("alice", "alice@example.com", "secret123")
print(account)  # Output: Account(username='alice', email='alice@example.com')
# Password não é mostrado, mas ainda é armazenado
print(account.password)  # Output: secret123

Isso é particularmente útil para dados sensíveis como senhas, chaves de API ou estruturas de dados grandes que deixariam a representação poluída.

Tornando Data Classes imutáveis com frozen=True

Por padrão, instâncias de data class são mutáveis — você pode mudar seus atributos após a criação. Se você quiser instâncias imutáveis (como tuplas), use frozen=True:

python
from dataclasses import dataclass
 
@dataclass(frozen=True)
class Point:
    x: float
    y: float
 
point = Point(3.0, 4.0)
print(point)  # Output: Point(x=3.0, y=4.0)
 
# Tentar modificar levanta um erro
try:
    point.x = 5.0
except AttributeError as e:
    print(f"Error: {e}")  # Output: Error: cannot assign to field 'x'

Data classes frozen são úteis quando você quer garantir a integridade dos dados ou usar instâncias como chaves de dicionário (já que chaves de dicionário precisam ser imutáveis). Quando uma data class é frozen, o Python também gera um método __hash__, tornando as instâncias hashable:

python
from dataclasses import dataclass
 
@dataclass(frozen=True)
class Coordinate:
    latitude: float
    longitude: float
 
# Instâncias frozen podem ser chaves de dicionário
locations = {
    Coordinate(40.7128, -74.0060): "New York",
    Coordinate(51.5074, -0.1278): "London",
    Coordinate(35.6762, 139.6503): "Tokyo"
}
 
nyc = Coordinate(40.7128, -74.0060)
print(locations[nyc])  # Output: New York

33.5) Inicialização personalizada com __post_init__

Às vezes você precisa fazer configuração adicional depois que o método __init__ gerado roda. O método __post_init__ é chamado automaticamente após a inicialização, permitindo que você valide dados, calcule atributos derivados ou execute outras tarefas de setup.

Uso básico de __post_init__

O método __post_init__ é chamado depois que todos os atributos foram definidos pelo __init__ gerado:

python
from dataclasses import dataclass
 
@dataclass
class Rectangle:
    width: float
    height: float
    area: float = 0.0  # Será calculado em __post_init__
    
    def __post_init__(self):
        """Calculate area after initialization."""
        self.area = self.width * self.height
 
rect = Rectangle(5.0, 3.0)
print(rect)  # Output: Rectangle(width=5.0, height=3.0, area=15.0)
print(f"Area: {rect.area}")  # Output: Area: 15.0

O método __post_init__ tem acesso a todos os atributos de instância que foram definidos durante a inicialização. Isso é útil para calcular valores derivados que dependem de múltiplos atributos.

Validando dados em post_init

Um uso comum de __post_init__ é validar se os dados fornecidos atendem a determinados requisitos:

python
from dataclasses import dataclass
 
@dataclass
class BankAccount:
    account_number: str
    balance: float
    
    def __post_init__(self):
        """Validate account data."""
        if self.balance < 0:
            raise ValueError("Balance cannot be negative")
 
# Conta válida
account1 = BankAccount("ACC001", 1000.0)
print(account1)  # Output: BankAccount(account_number='ACC001', balance=1000.0)
 
# Conta inválida - saldo negativo
try:
    account2 = BankAccount("ACC002", -500.0)
except ValueError as e:
    print(f"Error: {e}")  # Output: Error: Balance cannot be negative

Essa validação garante que instâncias estejam sempre em um estado válido. Se os dados não atendem aos requisitos, a instância nunca é criada, impedindo que objetos inválidos existam no seu programa.

Usando post_init com field(init=False)

Às vezes você quer um atributo que é calculado em __post_init__, mas não deve ser um parâmetro em __init__. Use field(init=False) para isso:

python
from dataclasses import dataclass, field
import math
 
@dataclass
class Circle:
    radius: float
    area: float = field(init=False)  # Não é um parâmetro em __init__
    circumference: float = field(init=False)
    
    def __post_init__(self):
        """Compute area and circumference from radius."""
        self.area = math.pi * self.radius ** 2
        self.circumference = 2 * math.pi * self.radius
 
# Só radius é exigido durante a inicialização
circle = Circle(5.0)
print(circle)  # Output: Circle(radius=5.0, area=78.53981633974483, circumference=31.41592653589793)
print(f"Area: {circle.area:.2f}")  # Output: Area: 78.54
print(f"Circumference: {circle.circumference:.2f}")  # Output: Circumference: 31.42

Esse padrão é útil quando você tem atributos que sempre são calculados a partir de outros atributos e nunca devem ser definidos diretamente durante a inicialização.


Data classes representam um recurso moderno do Python que reduz boilerplate enquanto mantém todo o poder das classes. Elas são particularmente valiosas para criar código limpo e legível ao trabalhar com dados estruturados. Conforme você continua aprendendo Python, você vai ver data classes se tornando uma escolha natural para muitas tarefas de programação centradas em dados, complementando as classes comuns que você aprendeu nos Capítulos 30-32.


© 2025. Primesoft Co., Ltd.
support@primesoft.ai