Pular para o conteúdo principal

Classes de dados Python: Um tutorial abrangente

Um tutorial amigável para iniciantes sobre classes de dados Python e como usá-las na prática
Actualizado 16 de jan. de 2025  · 9 min de leitura

As classes de dados são um dos recursos do Python que, depois de descobertos, você nunca mais voltará ao modo antigo. Considere esta classe regular:

class Exercise:
   def __init__(self, name, reps, sets, weight):
       self.name = name
       self.reps = reps
       self.sets = sets
       self.weight = weight

Para mim, essa definição de classe é muito ineficiente - no método __init__, você repete cada parâmetro pelo menos três vezes. Isso pode não parecer grande coisa, mas pense na frequência com que você escreve classes com muito mais parâmetros durante sua vida.

Em comparação, dê uma olhada na alternativa de classes de dados do código acima:

from dataclasses import dataclass


@dataclass
class Exercise:
   name: str
   reps: int
   sets: int
   weight: float  # Weight in lbs

Esse trecho de código de aparência modesta é muito melhor do que uma classe comum. O minúsculo decorador @dataclass está implementando as classes __init__, __repr__, __eq__ nos bastidores, o que exigiria pelo menos 20 linhas de código manualmente.

Além disso, muitos outros recursos, como operadores de comparação, ordenação de objetos e imutabilidade, estão a uma única linha de distância de serem magicamente criados para nossa classe.

Portanto, o objetivo deste tutorial é mostrar a você por que as classes de dados são uma das melhores coisas que aconteceram com o Python se você gosta de programação orientada a objetos.

Vamos começar!

Noções básicas sobre classes de dados Python

Vamos abordar alguns dos conceitos fundamentais das classes de dados do Python que as tornam tão úteis.

Alguns métodos são gerados automaticamente nas classes de dados

Apesar de todos os seus recursos, as classes de dados são classes comuns que exigem muito menos código para implementar a mesma funcionalidade. Aqui está a classe Exercise novamente:

from dataclasses import dataclass


@dataclass
class Exercise:
   name: str
   reps: int
   sets: int
   weight: float


ex1 = Exercise("Bench press", 10, 3, 52.5)

# Verifying Exercise is a regular class
ex1.name
'Bench press'

No momento, o site Exercise já tem os métodos __repr__ e __eq__ implementados. Vamos verificar isso:

repr(ex1)
"Exercise(name='Bench press', reps=10, sets=3, weight=52.5)"

A representação de um objeto repr deve retornar o código que pode recriar a si mesmo, e podemos ver que esse é exatamente o caso de ex1.

Em comparação, o site Exercise definido da maneira antiga teria a seguinte aparência:

class Exercise:
   def __init__(self, name, reps, sets, weight):
       self.name = name
       self.reps = reps
       self.sets = sets
       self.weight = weight


ex3 = Exercise("Bench press", 10, 3, 52.5)

ex3
<__main__.Exercise at 0x7f6834100130>

Parece horrível e inútil!

Agora, vamos verificar a existência de __eq__, que é o operador de igualdade:

# Redefine the class
@dataclass
class Exercise:
   name: str
   reps: int
   sets: int
   weight: float


ex1 = Exercise("Bench press", 10, 3, 52.5)
ex2 = Exercise("Bench press", 10, 3, 52.5)

A comparação da classe com ela mesma e com outra classe com parâmetros idênticos deve retornar True:

ex1 == ex2
True
ex1 == ex1
True

E é o que acontece! Em aulas normais, essa lógica teria sido muito difícil de escrever.

As classes de dados exigem dicas de tipo

Como você deve ter notado, as classes de dados exigem dicas de tipo ao definir campos. De fato, as classes de dados permitem qualquer tipo do módulo typing. Por exemplo, veja como criar um campo que pode aceitar o tipo de dados Any:

from typing import Any


@dataclass
class Dummy:
   attr: Any

No entanto, a idiossincrasia do Python é que, embora as classes de dados exijam dicas de tipo, os tipos não são realmente aplicados.

Por exemplo, a criação de uma instância da classe Exercise com tipos de dados completamente incorretos pode ser executada sem erros:

silly_exercise = Exercise("Bench press", "ten", "three sets", 52.5)

silly_exercise.sets

“Three sets”

Se quiser impor tipos de dados, você deverá usar verificadores de tipos como o Mypy.

As classes de dados permitem valores padrão nos campos

Até agora, não adicionamos nenhum padrão às nossas classes. Vamos corrigir isso:

@dataclass
class Exercise:
   name: str = "Push-ups"
   reps: int = 10
   sets: int = 3
   weight: float = 0


# Now, all fields have defaults
ex5 = Exercise()
ex5
Exercise(name='Push-ups', reps=10, sets=3, weight=0)

Lembre-se de que os campos não padrão não podem seguir os campos padrão. Por exemplo, o código abaixo gerará um erro:

@dataclass
class Exercise:
   name: str = "Push-ups"
   reps: int = 10
   sets: int = 3
   weight: float  # NOT ALLOWED


ex5 = Exercise()
ex5
TypeError: non-default argument 'weight' follows default argument

Na prática, você raramente definirá padrões com a sintaxe name: type = value.

Em vez disso, você usará a função field, que permite mais controle de cada definição de campo:

from dataclasses import field


@dataclass
class Exercise:
   name: str = field(default="Push-up")
   reps: int = field(default=10)
   sets: int = field(default=3)
   weight: float = field(default=0)


# Now, all fields have defaults
ex5 = Exercise()
ex5
Exercise(name='Push-up', reps=10, sets=3, weight=0)

A função field tem mais parâmetros, como, por exemplo, se você quiser que a função seja usada:

  • repr
  • init
  • compare
  • default_factory

e assim por diante. Discutiremos isso nas próximas seções.

As classes de dados podem ser criadas com uma função

Uma observação final sobre os fundamentos da classe de dados é que sua definição pode ser ainda mais curta se você usar a função make_dataclass:

from dataclasses import make_dataclass

Exercise = make_dataclass(
   "Exercise",
   [
       ("name", str),
       ("reps", int),
       ("sets", int),
       ("weight", float),
   ],
)

ex3 = Exercise("Deadlifts", 8, 3, 69.0)
ex3
Exercise(name='Deadlifts', reps=8, sets=3, weight=69.0)

Mas você sacrificará a legibilidade, por isso não recomendo usar essa função.

Classes de dados Python avançadas

Nesta seção, discutiremos os recursos avançados das classes de dados que trazem mais benefícios. Um desses recursos é uma fábrica padrão.

Fábricas padrão

Para explicar as fábricas padrão, vamos criar outra classe chamada WorkoutSession que aceita dois campos:

from dataclasses import dataclass
from typing import List


@dataclass
class Exercise:
   name: str = "Push-ups"
   reps: int = 10
   sets: int = 3
   weight: float = 0


@dataclass
class WorkoutSession:
   exercises: List[Exercise]
   duration_minutes: int

Ao usar o tipo List, estamos especificando que WorkoutSession aceita uma lista de instâncias de Exercise.

# Define the Exercise instances for HIIT training
ex1 = Exercise(name="Burpees", reps=15, sets=3)
ex2 = Exercise(name="Mountain Climbers", reps=20, sets=3)
ex3 = Exercise(name="Jump Squats", reps=12, sets=3)
exercises_monday = [ex1, ex2, ex3]

hiit_monday = WorkoutSession(exercises=exercises_monday, duration_minutes=30)

No momento, cada instância de sessão de treino exige que os exercícios sejam inicializados. Mas isso não reflete a forma como as pessoas se exercitam - primeiro, elas iniciam uma sessão (provavelmente em um aplicativo) e, em seguida, adicionam exercícios à medida que se exercitam.

Portanto, devemos ser capazes de criar sessões sem exercícios e sem duração. Vamos fazer isso adicionando uma lista vazia como um valor padrão para exercises:

@dataclass
class WorkoutSession:
   exercises: List[Exercise] = []
   duration_minutes: int = None


hiit_monday = WorkoutSession("25-02-2024")
ValueError: mutable default <class 'list'> for field exercises is not allowed: use default_factory

No entanto, recebemos um erro - acontece que as classes de dados não permitem valores padrão mutáveis.

Felizmente, podemos corrigir isso usando uma fábrica padrão:

@dataclass
class WorkoutSession:
   exercises: List[Exercise] = field(default_factory=list)  # PAY ATTENTION
   duration_minutes: int = 0


hiit_monday = WorkoutSession()
hiit_monday
WorkoutSession(exercises=[], duration_minutes=0)

O parâmetro default_factory aceita uma função que retorna um valor inicial para um campo de classe de dados. Isso significa que ele pode aceitar qualquer função arbitrária:

  • tuple
  • dict
  • set
  • Qualquer função personalizada definida pelo usuário

Isso é preciso, independentemente de o resultado da função ser mutável ou não.

Agora, se pensarmos bem, a maioria das pessoas começa seu treinamento com exercícios de aquecimento que são normalmente semelhantes para qualquer tipo de treino. Portanto, inicializar sessões sem exercícios pode não ser o que algumas pessoas desejam.

Em vez disso, vamos criar uma função que retorne três aquecimentos Exercises:

def create_warmup():
   return [
       Exercise("Jumping jacks", 30, 1),
       Exercise("Squat lunges", 10, 2),
       Exercise("High jumps", 20, 1),
   ]

@dataclass
class WorkoutSession:
   exercises: List[Exercise] = field(default_factory=create_warmup)
   duration_minutes: int = 5  # Increase the default duration as well


hiit_monday = WorkoutSession()
hiit_monday

WorkoutSession(exercises=[Exercise(name='Jumping jacks', reps=30, sets=1, weight=0), Exercise(name='Squat lunges', reps=10, sets=2, weight=0), Exercise(name='High jumps', reps=20, sets=1, weight=0)], duration_minutes=5)

Agora, sempre que criarmos uma sessão, ela virá com alguns exercícios de aquecimento já registrados. A nova versão do WorkoutSession tem uma duração padrão de cinco minutos para levar isso em conta.

Adição de métodos a classes de dados

Como as classes de dados são classes comuns, você pode adicionar métodos a elas da mesma forma. Vamos adicionar dois métodos à nossa classe de dados WorkoutSession:

@dataclass
class WorkoutSession:
   exercises: List[Exercise] = field(default_factory=create_warmup)
   duration_minutes: int = 5

   def add_exercise(self, exercise: Exercise):
       self.exercises.append(exercise)

   def increase_duration(self, minutes: int):
       self.duration_minutes += minutes

Usando esses métodos, agora podemos registrar qualquer nova atividade em uma sessão:

hiit_monday = WorkoutSession()

# Log a new exercise
new_exercise = Exercise("Deadlifts", 6, 4, 60)

hiit_monday.add_exercise(new_exercise)
hiit_monday.increase_duration(15)

Mas há um problema:

hiit_monday

WorkoutSession(exercises=[Exercise(name='Jumping jacks', reps=30, sets=1, weight=0), Exercise(name='Squat lunges', reps=10, sets=2, weight=0), Exercise(name='High jumps', reps=20, sets=1, weight=0), Exercise(name='Deadlifts', reps=6, sets=4, weight=60)], duration_minutes=20)

Quando imprimimos a sessão, sua representação padrão é muito detalhada e ilegível, pois contém o código para recriar o objeto. Vamos corrigir isso.

__repr__ e __str__ em classes de dados

As classes de dados implementam __repr__ automaticamente, mas não __str__. Isso faz com que a classe volte para __repr__ quando chamamos o print para ela.

Portanto, vamos substituir esse comportamento definindo __str__ por conta própria:

@dataclass
class Exercise:
   name: str = "Push-ups"
   reps: int = 10
   sets: int = 3
   weight: float = 0

   def __str__(self):
       base = f"{self.name}: {self.reps}/{self.sets}"
       if self.weight == 0:
           return base
       return base + f", {self.weight} lbs"


ex1 = Exercise(name="Burpees", reps=15, sets=3)
ex1
Exercise(name='Burpees', reps=15, sets=3, weight=0)

O __repr__ ainda é o mesmo, mas quando ligamos para o print, você pode ver que o é o mesmo:

print(ex1)
Burpees: 15/3

A representação da classe na primavera é muito mais agradável. Agora, vamos corrigir também o WorkoutSession:

@dataclass
class WorkoutSession:
   exercises: List[Exercise] = field(default_factory=create_warmup)
   duration_minutes: int = 5  # Increase the default duration as well

   def add_exercise(self, exercise: Exercise):
       self.exercises.append(exercise)

   def increase_duration(self, minutes: int):
       self.duration_minutes += minutes

   def __str__(self):
       base = ""

       for ex in self.exercises:
           base += str(ex) + "\n"
       base += f"\nSession duration: {self.duration_minutes} minutes."

       return base


hiit_monday = WorkoutSession()
print(hiit_monday)

Jumping jacks: 30/1
Squat lunges: 10/2
High jumps: 20/1

Session duration: 5 minutes.

Observação: Use o botão "Explain code" (Explicar código) na parte inferior do snippet para obter uma explicação linha por linha do código.

Agora, você tem um resultado legível e compacto.

Comparação em classes de dados

Para muitas classes, faz sentido comparar seus objetos por alguma lógica. Para exercícios, pode ser a duração do exercício, a intensidade do exercício ou o peso.

Primeiro, vamos ver o que acontece se tentarmos comparar dois exercícios no estado atual:

hiit_wednesday = WorkoutSession()

hiit_wednesday.add_exercise(Exercise("Pull-ups", 7, 3))
print(hiit_wednesday)

Jumping jacks: 30/1
Squat lunges: 10/2
High jumps: 20/1
Pull-ups: 7/3

Session duration: 5 minutes.

hiit_monday > hiit_wednesday
TypeError: '>' not supported between instances of 'WorkoutSession' and 'WorkoutSession'

Recebemos um TypeError, pois as classes de dados não implementam operadores de comparação. Mas isso pode ser facilmente corrigido se você definir o parâmetro order como True:

@dataclass(order=True)
class WorkoutSession:
   exercises: List[Exercise] = field(default_factory=create_warmup)
   duration_minutes: int = 5

   ...

hiit_monday = WorkoutSession()
# hiit_monday.add_exercise(...)
hiit_monday.increase_duration(10)

hiit_wednesday = WorkoutSession()

hiit_monday > hiit_wednesday

True

Desta vez, a comparação funciona, mas o que estamos comparando?

Nas classes de dados, a comparação é realizada na ordem em que os campos são definidos. No momento, as classes são comparadas com base na duração do exercício, pois o primeiro campo, exercises, contém objetos não padronizados.

Podemos verificar isso aumentando a duração da sessão de quarta-feira:

hiit_monday = WorkoutSession()
# hiit_monday.add_exercise(...)

hiit_wednesday = WorkoutSession()
hiit_wednesday.increase_duration(10)

hiit_monday > hiit_wednesday
False

Como esperado, recebemos o e-mail False.

Mas o que aconteceria se o primeiro campo de Workout fosse outro tipo de campo, por exemplo, uma cadeia de caracteres? Vamos tentar descobrir:

@dataclass(order=True)
class WorkoutSession:
   date: str = None  # DD-MM-YYYY
   exercises: List[Exercise] = field(default_factory=create_warmup)
   duration_minutes: int = 5

   ...

hiit_monday = WorkoutSession("25-02-2024")
hiit_monday.increase_duration(10)

hiit_wednesday = WorkoutSession("27-02-2024")

hiit_monday > hiit_wednesday
False

Embora a sessão de segunda-feira dure mais tempo, a comparação nos diz que ela é menor do que a de quarta-feira. O motivo é que "25" vem antes de "27" na comparação de strings do Python.

Então, como podemos manter a ordem dos campos e ainda classificar as sessões com base na duração do exercício? Isso é fácil por meio da função field:

@dataclass(order=True)
class WorkoutSession:
   date: str = field(default=None, compare=False)
   exercises: List[Exercise] = field(default_factory=create_warmup)
   duration_minutes: int = 5

   ...

hiit_monday = WorkoutSession("25-02-2024")
hiit_monday.increase_duration(10)

hiit_wednesday = WorkoutSession("27-02-2024")

hiit_monday > hiit_wednesday
True

Ao definir compare como False para qualquer campo, nós o excluímos da classificação, conforme evidenciado pelo resultado acima.

Manipulação de campo pós-inicialização

No momento, temos uma duração de sessão padrão de cinco minutos para levar em conta os exercícios de aquecimento. No entanto, isso só faz sentido se o usuário iniciar uma sessão com um aquecimento. E se você começar uma sessão com outros exercícios?

new_session = WorkoutSession([Exercise("Diamond push-ups", 10, 3)])

new_session.duration_minutes
5

Para apenas um único exercício, a duração total é de cinco minutos, o que não tem lógica. Cada sessão deve adivinhar dinamicamente sua duração com base no número de séries de cada exercício. Isso significa que devemos tornar o duration_minutes dependente do campo exercises.

Vamos implementá-lo:

@dataclass
class WorkoutSession:
   exercises: List[Exercise] = field(default_factory=create_warmup)
   duration_minutes: int = field(default=0, init=False)

   def __post_init__(self):
       set_duration = 3
       for ex in self.exercises:
           self.duration_minutes += ex.sets * set_duration

   ...

Desta vez, estamos definindo duration_minutes com init definido como False para atrasar a inicialização do campo.

Em seguida, dentro de um método especial __post_init__, estamos atualizando seu valor com base no número total de conjuntos em cada Exercise.

Agora, quando inicializamos WorkoutSession, o duration_minutes é aumentado dinamicamente em três minutos para cada série em cada exercício.

# Adding an exercise with three sets
hiit_friday = WorkoutSession([Exercise("Diamond push-ups", 10, 3)])

hiit_friday.duration_minutes
9

Em geral, se você quiser definir um campo que dependa de outros campos da sua classe de dados, poderá usar a lógica __post_init__.

Imutabilidade em classes de dados

Nossa classe de dados WorkoutSession está quase pronta; ela só precisa ser protegida. No momento, você pode bagunçá-lo facilmente:

hiit_friday.duration_minutes = 1000

hiit_friday.duration_minutes
1000

del hiit_friday.exercises

Queremos proteger todos os campos de nossas classes para que eles possam ser modificados somente da maneira que desejamos. Para isso, o decorador @dataclass oferece o conveniente argumento frozen:

@dataclass(frozen=True)
class FrozenExercise:
   name: str
   reps: int
   sets: int
   weight: int | float = 0


ex1 = FrozenExercise("Muscle-ups", 5, 3)

Agora, se quisermos modificar qualquer campo, receberemos um erro:

ex1.sets = 5
FrozenInstanceError: cannot assign to field 'sets'

Ao definir frozen como True, você adiciona automaticamente os métodos __deleteattr__ e __setattr__ para cada campo, de modo que eles fiquem protegidos contra exclusão ou atualizações após a inicialização. Além disso, outras pessoas também não poderão adicionar novos campos:

ex1.new_field = 10
FrozenInstanceError: cannot assign to field 'new_field'

Essa funcionalidade teria dezenas de linhas de código se estivéssemos lidando com classes tradicionais.

No entanto, observe que não é possível tornar nossas classes realmente imutáveis. Por exemplo, vamos reescrever o WorkoutSession com frozen definido como True:

@dataclass(frozen=True)
class ImmutableWorkoutSession:
   exercises: List[Exercise] = field(default_factory=create_warmup)
   duration_minutes: int = 5


session1 = ImmutableWorkoutSession()

Como esperado, não podemos modificar diretamente a lista de exercícios:

session1.exercises = [Exercise()]

No entanto, exercises é uma lista totalmente mutável, o que torna possível a seguinte operação:

# Alteração de um dos elementos em uma lista

# Changing one of the elements in a list
session1.exercises[1] = FrozenExercise("Totally new exercise", 5, 5)

print(session1)

ImmutableWorkoutSession(exercises=[Exercise(name='Jumping jacks', reps=30, sets=1, weight=0), FrozenExercise(name='Totally new exercise', reps=5, sets=5, weight=0), Exercise(name='High jumps', reps=20, sets=1, weight=0)], duration_minutes=5)

Portanto, para proteger você de alterações acidentais, é recomendável usar objetos imutáveis, como tuplas, para os valores de campo.

Herança em classes de dados

Um último ponto que abordaremos é a ordem dos campos nas classes pai e filho.

Como as classes de dados são classes comuns, a herança funciona normalmente:

@dataclass(frozen=True)
class ImmutableWorkoutSession:
   exercises: List[Exercise] = field(default_factory=create_warmup)
   duration_minutes: int = 5


@dataclass(frozen=True)
class CardioWorkoutSession(ImmutableWorkoutSession):
   pass

Porém, como o último campo da classe principal (ImmutableWorkoutSession) tem um valor padrão, todos os campos das classes secundárias devem ter valores padrão.

Por exemplo, isso não é permitido:

@dataclass(frozen=True)
class ImmutableWorkoutSession:
   exercises: List[Exercise] = field(default_factory=create_warmup)
   duration_minutes: int = 5


@dataclass(frozen=True)
class CardioWorkoutSession(ImmutableWorkoutSession):
   intensity_level: str  # Not allowed, must have a default

TypeError: non-default argument 'intensity_level' follows default argument

Desvantagens das classes de dados e outros recursos

As classes de dados têm melhorado constantemente desde o Python 3.7 (elas eram ótimas para começar) e abrangem muitos casos de uso em que você pode precisar escrever classes. Mas eles podem ser desvantajosos nos seguintes cenários:

  • Métodos personalizados do site __init__
  • Métodos personalizados do site __new__
  • Vários padrões de herança

E muitos outros, conforme discutido neste excelente tópico do Reddit. Se você quiser uma justificativa mais detalhada sobre o motivo pelo qual as classes de dados foram introduzidas e por que elas não são substitutas das definições de classes regulares, leia a PEP 557.

Se você estiver interessado em programação orientada a objetos em geral, este é um curso para continuar sua jornada:

Basicamente, as classes de dados são estruturas mais sofisticadas para armazenar e recuperar dados com mais eficiência. No entanto, o Python tem muitas outras estruturas de dados que realizam essa tarefa de maneira mais ou menos semelhante. Por exemplo, você pode aprender sobre contadores, defaultdicts e namedtuples no último capítulo do curso Tipos de dados para ciência de dados.


Bex Tuychiev's photo
Author
Bex Tuychiev
LinkedIn

Sou um criador de conteúdo de ciência de dados com mais de 2 anos de experiência e um dos maiores seguidores no Medium. Gosto de escrever artigos detalhados sobre IA e ML com um estilo um pouco sarcástico, porque você precisa fazer algo para torná-los um pouco menos monótonos. Produzi mais de 130 artigos e um curso DataCamp, e estou preparando outro. Meu conteúdo foi visto por mais de 5 milhões de pessoas, das quais 20 mil se tornaram seguidores no Medium e no LinkedIn. 

Temas

Continue aprendendo Python

curso

Intermediate Python

4 hr
1.2M
Level up your data science skills by creating visualizations using Matplotlib and manipulating DataFrames with pandas.
Ver DetalhesRight Arrow
Iniciar curso
Ver maisRight Arrow
Relacionado

tutorial

Programação orientada a objetos em Python (OOP): Tutorial

Aborde os fundamentos da programação orientada a objetos (OOP) em Python: explore classes, objetos, métodos de instância, atributos e muito mais!
Théo Vanderheyden's photo

Théo Vanderheyden

12 min

tutorial

Tutorial de Python

Em Python, tudo é objeto. Números, cadeias de caracteres (strings), DataFrames, e até mesmo funções são objetos. Especificamente, qualquer coisa que você usa no Python tem uma classe, um modelo associado por trás.
DataCamp Team's photo

DataCamp Team

3 min

tutorial

Tutorial de funções Python

Um tutorial sobre funções em Python que aborda como escrever funções, como chamá-las e muito mais!
Karlijn Willems's photo

Karlijn Willems

14 min

tutorial

Tutorial de conversão de tipos de dados do Python

Neste tutorial de Python, você abordará a conversão implícita e explícita de tipos de dados de estruturas de dados primitivas e não primitivas com a ajuda de exemplos de código!
Sejal Jaiswal's photo

Sejal Jaiswal

13 min

tutorial

Tutorial de conjuntos e teoria de conjuntos em Python

Aprenda sobre os conjuntos do Python: o que são, como criá-los, quando usá-los, funções incorporadas e sua relação com as operações da teoria dos conjuntos.
DataCamp Team's photo

DataCamp Team

13 min

Ver maisVer mais