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
@dataclassclassExercise:
name:str
reps:int
sets:int
weight:float# Weight in lbsTecnologia
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
@dataclassclassExercise:
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'Tecnologia
No momento, o site Exercise já tem os métodos __repr__ e __eq__ implementados. Vamos verificar isso:
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
@dataclassclassDummy:
attr: AnyTecnologia
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:
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:
@dataclassclassWorkoutSession:
exercises: List[Exercise]=[]
duration_minutes:int=None
hiit_monday = WorkoutSession("25-02-2024")
ValueError: mutable default <class'list'>for field exercises isnot allowed: use default_factoryTecnologia
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:
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:
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:
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:
@dataclassclassExercise:
name:str="Push-ups"
reps:int=10
sets:int=3
weight:float=0def__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)Tecnologia
O __repr__ ainda é o mesmo, mas quando ligamos para o print, você pode ver que o é o mesmo:
A representação da classe na primavera é muito mais agradável. Agora, vamos corrigir também o WorkoutSession:
@dataclassclassWorkoutSession:
exercises: List[Exercise]= field(default_factory=create_warmup)
duration_minutes:int=5# Increase the default duration as welldefadd_exercise(self, exercise: Exercise):
self.exercises.append(exercise)defincrease_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.Tecnologia
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'Tecnologia
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:
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:
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:
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?
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:
@dataclassclassWorkoutSession:
exercises: List[Exercise]= field(default_factory=create_warmup)
duration_minutes:int= field(default=0, init=False)def__post_init__(self):
set_duration =3for ex in self.exercises:
self.duration_minutes += ex.sets * set_duration
...Tecnologia
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
9Tecnologia
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:
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:
Agora, se quisermos modificar qualquer campo, receberemos um erro:
ex1.sets =5
FrozenInstanceError: cannot assign to field 'sets'Tecnologia
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'Tecnologia
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:
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)Tecnologia
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:
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)classImmutableWorkoutSession:
exercises: List[Exercise]= field(default_factory=create_warmup)
duration_minutes:int=5@dataclass(frozen=True)classCardioWorkoutSession(ImmutableWorkoutSession):
intensity_level:str# Not allowed, must have a default
TypeError: non-default argument 'intensity_level' follows default argument
Tecnologia
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.
Introdução às estruturas de dados do Python: saiba mais sobre tipos de dados e estruturas de dados primitivas e não primitivas, como strings, listas, pilhas etc.
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.
Descubra como escrever funções em Python reutilizáveis e eficientes. Domine parâmetros, instruções de retorno e temas avançados como funções lambda. Organize melhor seu código com main() e outras boas práticas.
Aprenda sobre classes abstratas em Python, sua finalidade e como usar o módulo `abc` para garantir interfaces consistentes. Inclui exemplos práticos e melhores práticas para uma implementação eficaz.
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:
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:
Esse trecho de código de aparência modesta é muito melhor do que uma classe comum. O minúsculo decorador
@dataclassestá 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
Exercisenovamente:No momento, o site
Exercisejá tem os métodos__repr__e__eq__implementados. Vamos verificar isso:A representação de um objeto
reprdeve retornar o código que pode recriar a si mesmo, e podemos ver que esse é exatamente o caso deex1.Em comparação, o site
Exercisedefinido da maneira antiga teria a seguinte aparência:Parece horrível e inútil!
Agora, vamos verificar a existência de
__eq__, que é o operador de igualdade:A comparação da classe com ela mesma e com outra classe com parâmetros idênticos deve retornar 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 dadosAny: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
Exercisecom tipos de dados completamente incorretos pode ser executada sem erros: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:
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:
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:A função
fieldtem mais parâmetros, como, por exemplo, se você quiser que a função seja usada:reprinitcomparedefault_factorye 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: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
WorkoutSessionque aceita dois campos:Ao usar o tipo
List, estamos especificando queWorkoutSessionaceita uma lista de instâncias deExercise.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: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:
O parâmetro
default_factoryaceita 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:tupledictsetIsso é 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:Agora, sempre que criarmos uma sessão, ela virá com alguns exercícios de aquecimento já registrados. A nova versão do
WorkoutSessiontem 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:Usando esses métodos, agora podemos registrar qualquer nova atividade em uma sessão:
Mas há um problema:
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 dadosAs 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:O
__repr__ainda é o mesmo, mas quando ligamos para oprint, você pode ver que o é o mesmo:A representação da classe na primavera é muito mais agradável. Agora, vamos corrigir também o
WorkoutSession: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:
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âmetroordercomoTrue: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:
Como esperado, recebemos o e-mail
False.Mas o que aconteceria se o primeiro campo de
Workoutfosse outro tipo de campo, por exemplo, uma cadeia de caracteres? Vamos tentar descobrir: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:Ao definir
comparecomoFalsepara 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?
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_minutesdependente do campoexercises.Vamos implementá-lo:
Desta vez, estamos definindo
duration_minutescominitdefinido comoFalsepara 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 cadaExercise.Agora, quando inicializamos
WorkoutSession, oduration_minutesé aumentado dinamicamente em três minutos para cada série em cada exercício.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
WorkoutSessionestá quase pronta; ela só precisa ser protegida. No momento, você pode bagunçá-lo facilmente:Queremos proteger todos os campos de nossas classes para que eles possam ser modificados somente da maneira que desejamos. Para isso, o decorador
@dataclassoferece o conveniente argumentofrozen:Agora, se quisermos modificar qualquer campo, receberemos um erro:
Ao definir
frozencomoTrue, 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: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
WorkoutSessioncomfrozendefinido comoTrue:Como esperado, não podemos modificar diretamente a lista de exercícios:
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
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:
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:
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:
__init____new__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,
defaultdictsenamedtuplesno último capítulo do curso Tipos de dados para ciência de dados.