Les classes de données sont l'une des caractéristiques de Python qui, une fois que vous les avez découvertes, ne vous permet plus de revenir à l'ancienne méthode. Considérez cette classe régulière :
classExercise:def__init__(self, name, reps, sets, weight):
self.name = name
self.reps = reps
self.sets = sets
self.weight = weight
Propulsé par
Pour moi, cette définition de classe est très inefficace - dans la méthode __init__, vous répétez chaque paramètre au moins trois fois. Cela peut sembler anodin, mais pensez au nombre de fois où vous écrivez des classes avec beaucoup plus de paramètres.
En comparaison, jetez un coup d'œil à l'alternative des classes de données du code ci-dessus :
from dataclasses import dataclass
@dataclassclassExercise:
name:str
reps:int
sets:int
weight:float# Weight in lbsPropulsé par
Ce morceau de code d'apparence modeste est bien meilleur qu'une classe ordinaire. Le minuscule décorateur @dataclass implémente les classes __init__, __repr__, __eq__ dans les coulisses, ce qui aurait nécessité au moins 20 lignes de code manuellement.
En outre, de nombreuses autres fonctionnalités, telles que les opérateurs de comparaison, l'ordonnancement des objets et l'immutabilité, sont toutes à une ligne près d'être créées comme par magie pour notre classe.
Le but de ce tutoriel est donc de vous montrer pourquoi les classes de données sont l'une des meilleures choses qui soient arrivées à Python si vous aimez la programmation orientée objet.
Commençons !
Les bases des classes de données Python
Abordons quelques-uns des concepts fondamentaux des classes de données Python qui les rendent si utiles.
Certaines méthodes sont automatiquement générées dans les classes de données
Malgré toutes leurs caractéristiques, les classes de données sont des classes normales qui nécessitent beaucoup moins de code pour mettre en œuvre la même fonctionnalité. Voici à nouveau la classe Exercise:
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'Propulsé par
À l'heure actuelle, Exercise dispose déjà des méthodes __repr__ et __eq__. Vérifions-le :
repr(ex1)"Exercise(name='Bench press', reps=10, sets=3, weight=52.5)"Propulsé par
La représentation d'un objet repr doit renvoyer le code qui peut se recréer lui-même, et nous pouvons voir que c'est exactement le cas pour ex1.
En comparaison, Exercise défini selon l'ancienne méthode ressemblerait à ceci :
classExercise: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>Propulsé par
Il a l'air assez horrible et inutile !
Vérifions maintenant l'existence de __eq__, qui est l'opérateur d'égalité :
# Redefine the class@dataclassclassExercise:
name:str
reps:int
sets:int
weight:float
ex1 = Exercise("Bench press",10,3,52.5)
ex2 = Exercise("Bench press",10,3,52.5)Propulsé par
La comparaison de la classe à elle-même et à une autre classe avec des paramètres identiques doit renvoyer True :
Et c'est le cas ! Dans les classes ordinaires, cette logique aurait été pénible à écrire.
Les classes de données nécessitent des indications de type
Comme vous l'avez peut-être remarqué, les classes de données nécessitent des indications de type lors de la définition des champs. En fait, les classes de données autorisent n'importe quel type du module typing. Par exemple, voici comment créer un champ qui peut accepter le type de données Any:
from typing import Any
@dataclassclassDummy:
attr: AnyPropulsé par
Cependant, l'idiosyncrasie de Python est que même si les classes de données nécessitent des indications de type, les types ne sont pas réellement imposés.
Par exemple, la création d'une instance de la classe Exercise avec des types de données totalement incorrects peut être exécutée sans erreur :
silly_exercise = Exercise("Bench press","ten","three sets",52.5)
silly_exercise.sets
“Three sets”Propulsé par
Si vous souhaitez renforcer les types de données, vous devez utiliser des vérificateurs de types tels que Mypy.
Les classes de données autorisent les valeurs par défaut dans les champs
Jusqu'à présent, nous n'avons pas ajouté de valeurs par défaut à nos classes. Corrigeons cela :
@dataclassclassExercise:
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)Propulsé par
N'oubliez pas que les champs qui ne sont pas des champs par défaut ne peuvent pas suivre les champs par défaut. Par exemple, le code ci-dessous provoquera une erreur :
@dataclassclassExercise:
name:str="Push-ups"
reps:int=10
sets:int=3
weight:float# NOT ALLOWED
ex5 = Exercise()
ex5
TypeError: non-default argument 'weight' follows default argumentPropulsé par
Dans la pratique, vous définirez rarement des valeurs par défaut à l'aide de la syntaxe name: type = value.
Vous utiliserez plutôt la fonction field, qui permet de mieux contrôler chaque définition de champ :
from dataclasses import field
@dataclassclassExercise:
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)Propulsé par
La fonction field a plus de paramètres, tels que
repr
init
compare
default_factory
et ainsi de suite. Nous les examinerons dans les sections suivantes.
Les classes de données peuvent être créées à l'aide d'une fonction
Une dernière remarque sur les bases des classes de données est que leur définition peut être encore plus courte en utilisant la fonction 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)Propulsé par
Mais vous sacrifierez la lisibilité, c'est pourquoi je ne recommande pas l'utilisation de cette fonction.
Classes de données avancées en Python
Dans cette section, nous aborderons les fonctionnalités avancées des classes de données qui apportent davantage d'avantages. L'une de ces caractéristiques est une usine par défaut.
Usines par défaut
Pour expliquer les usines par défaut, créons une autre classe nommée WorkoutSession qui accepte deux champs :
from dataclasses import dataclass
from typing import List
@dataclassclassExercise:
name:str="Push-ups"
reps:int=10
sets:int=3
weight:float=0@dataclassclassWorkoutSession:
exercises: List[Exercise]
duration_minutes:intPropulsé par
En utilisant le type List, nous spécifions que WorkoutSession accepte une liste d'instances 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)Propulsé par
Actuellement, chaque instance de séance d'entraînement nécessite l'initialisation des exercices. Mais cela ne reflète pas la façon dont les gens s'entraînent - ils commencent d'abord une session (probablement dans une application), puis ils ajoutent des exercices au fur et à mesure qu'ils s'entraînent.
Il faut donc pouvoir créer des sessions sans exercices et sans durée. Pour ce faire, ajoutons une liste vide comme valeur par défaut pour 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_factoryPropulsé par
Cependant, nous avons obtenu une erreur - il s'avère que les classes de données n'autorisent pas les valeurs par défaut mutables.
Heureusement, il est possible de remédier à ce problème en utilisant une usine par défaut :
Le paramètre default_factory accepte une fonction qui renvoie une valeur initiale pour un champ de la classe de données. Cela signifie qu'il peut accepter n'importe quelle fonction arbitraire :
tuple
dict
set
Toute fonction personnalisée définie par l'utilisateur
Ceci est exact, que le résultat de la fonction soit mutable ou non.
Si l'on y réfléchit bien, la plupart des gens commencent leur entraînement par des exercices d'échauffement qui sont généralement similaires pour tout type de séance d'entraînement. Par conséquent, l'initialisation des sessions sans exercices peut ne pas correspondre à ce que certaines personnes souhaitent.
Au lieu de cela, créons une fonction qui renvoie trois sites d'échauffement Exercises:
defcreate_warmup():return[
Exercise("Jumping jacks",30,1),
Exercise("Squat lunges",10,2),
Exercise("High jumps",20,1),]@dataclassclassWorkoutSession:
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)Propulsé par
Désormais, chaque fois que nous créons une session, les participants sont accompagnés d'exercices d'échauffement déjà enregistrés. La nouvelle version de WorkoutSession a une durée par défaut de cinq minutes pour en tenir compte.
Ajouter des méthodes aux classes de données
Les classes de données étant des classes normales, l'ajout de méthodes reste le même. Ajoutons deux méthodes à notre classe de données WorkoutSession:
Grâce à ces méthodes, nous pouvons désormais enregistrer toute nouvelle activité dans une session :
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)Propulsé par
Lorsque nous imprimons la session, sa représentation par défaut est trop verbeuse et illisible puisqu'elle contient le code pour recréer l'objet. Corrigeons cela.
__repr__ et __str__ dans les classes de données
Les classes de données implémentent automatiquement __repr__ mais pas __str__. Cela permet à la classe de se rabattre sur __repr__ lorsque nous faisons appel à print.
Remplaçons donc ce comportement en définissant nous-mêmes __str__:
@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)Propulsé par
Le site __repr__ est toujours le même, mais lorsque nous appelons print sur ce site :
La représentation printanière de la classe est beaucoup plus agréable. Maintenant, corrigeons également 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.Propulsé par
Note: Utilisez le bouton "Expliquer le code" au bas de l'extrait pour obtenir une explication ligne par ligne du code.
Nous avons maintenant un résultat lisible et compact.
Comparaison des classes de données
Pour de nombreuses classes, il est judicieux de comparer leurs objets selon une certaine logique. Pour les séances d'entraînement, il peut s'agir de la durée de la séance, de l'intensité de l'exercice ou du poids.
Tout d'abord, voyons ce qui se passe si nous essayons de comparer deux séances d'entraînement dans l'état actuel :
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'Propulsé par
Nous recevons TypeError car les classes de données n'implémentent pas d'opérateurs de comparaison. Mais cela peut être facilement corrigé en réglant le paramètre order sur True:
Cette fois-ci, la comparaison fonctionne, mais qu'est-ce qu'on compare ?
Dans les classes de données, la comparaison est effectuée dans l'ordre dans lequel les champs sont définis. Actuellement, les classes sont comparées sur la base de la durée de l'entraînement car le premier champ, exercises, contient des objets non standard.
Nous pouvons le vérifier en augmentant la durée de la session de mercredi :
Même si la session du lundi dure plus longtemps, la comparaison montre qu'elle est plus petite que celle du mercredi. La raison en est que "25" vient avant "27" dans la comparaison de chaînes de Python.
Comment conserver l'ordre des champs tout en triant les sessions en fonction de la durée de la séance d'entraînement ? Cette opération est facile à réaliser grâce à la fonction field:
En définissant compare sur False pour un champ quelconque, nous l'excluons du tri, comme le montre le résultat ci-dessus.
Manipulation du champ post-init
À l'heure actuelle, la durée de la session est fixée par défaut à cinq minutes pour tenir compte des exercices d'échauffement. Toutefois, cela n'a de sens que si l'utilisateur commence une session par un échauffement. Que se passe-t-il s'ils commencent une session avec d'autres exercices ?
new_session = WorkoutSession([Exercise("Diamond push-ups",10,3)])
new_session.duration_minutes
5Propulsé par
Pour un seul exercice, la durée totale est de cinq minutes, ce qui est illogique. Chaque session doit déterminer dynamiquement sa durée en fonction du nombre de séries de chaque exercice. Cela signifie que nous devons rendre duration_minutes dépendant du champ exercises.
Mettons-le en œuvre :
@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
...Propulsé par
Cette fois, nous définissons duration_minutes avec init fixé à False pour retarder l'initialisation du champ.
Ensuite, à l'intérieur d'une méthode spéciale __post_init__, nous mettons à jour sa valeur en fonction du nombre total d'ensembles dans chaque Exercise.
Maintenant, lorsque nous initialisons WorkoutSession, le site duration_minutes est dynamiquement augmenté de trois minutes pour chaque série de chaque exercice.
# Adding an exercise with three sets
hiit_friday = WorkoutSession([Exercise("Diamond push-ups",10,3)])
hiit_friday.duration_minutes
9Propulsé par
En général, si vous souhaitez définir un champ qui dépend d'autres champs de votre classe de données, vous pouvez utiliser la logique __post_init__.
L'immutabilité dans les classes de données
Notre classe de données WorkoutSession est presque prête ; il ne reste plus qu'à la protéger. À l'heure actuelle, il est assez facile d'y mettre le bazar :
hiit_friday.duration_minutes =1000
hiit_friday.duration_minutes
1000del hiit_friday.exercisesPropulsé par
Nous voulons protéger tous les champs de nos classes afin qu'ils ne puissent être modifiés qu'à notre guise. Pour ce faire, le décorateur @dataclass propose un argument pratique frozen:
@dataclass(frozen=True)classFrozenExercise:
name:str
reps:int
sets:int
weight:int|float=0
ex1 = FrozenExercise("Muscle-ups",5,3)Propulsé par
Maintenant, si nous voulons modifier un champ, nous obtenons une erreur :
ex1.sets =5
FrozenInstanceError: cannot assign to field 'sets'Propulsé par
La définition de frozen en True ajoute automatiquement les méthodes __deleteattr__ et __setattr__ pour chaque champ afin qu'ils soient protégés contre la suppression ou les mises à jour après l'initialisation. En outre, les autres ne pourront pas non plus ajouter de nouveaux champs :
ex1.new_field =10
FrozenInstanceError: cannot assign to field 'new_field'Propulsé par
Cette fonctionnalité comporterait des dizaines de lignes de code si nous avions affaire à des classes traditionnelles.
Notez toutefois que nous ne pouvons pas rendre nos classes réellement immuables. Par exemple, réécrivons le site WorkoutSession en remplaçant frozen par True:
@dataclass(frozen=True)classImmutableWorkoutSession:
exercises: List[Exercise]= field(default_factory=create_warmup)
duration_minutes:int=5
session1 = ImmutableWorkoutSession()Propulsé par
Comme prévu, nous ne pouvons pas modifier directement la liste des exercices :
Cependant, exercises est une liste, qui est entièrement mutable, ce qui rend l'opération suivante possible :
# Changement d'un des éléments d'une liste
# 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)Propulsé par
Pour se prémunir contre les modifications accidentelles, il est donc recommandé d'utiliser des objets immuables, tels que des tuples, pour les valeurs des champs.
Héritage dans les classes de données
Un dernier point que nous aborderons est l'ordre des champs dans les classes parentales et enfantines.
Comme les classes de données sont des classes normales, l'héritage fonctionne comme d'habitude :
@dataclass(frozen=True)classImmutableWorkoutSession:
exercises: List[Exercise]= field(default_factory=create_warmup)
duration_minutes:int=5@dataclass(frozen=True)classCardioWorkoutSession(ImmutableWorkoutSession):passPropulsé par
Mais comme le dernier champ de la classe mère (ImmutableWorkoutSession) a une valeur par défaut, tous les champs des classes enfants doivent avoir des valeurs par défaut.
Par exemple, ceci n'est pas autorisé :
@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
Propulsé par
Inconvénients des classes de données et autres ressources
Les classes de données ont été constamment améliorées depuis Python 3.7 (elles étaient excellentes au départ) et couvrent de nombreux cas d'utilisation où vous pourriez avoir besoin d'écrire des classes. Mais ils peuvent être désavantageux dans les scénarios suivants :
Méthodes personnalisées __init__
Méthodes personnalisées __new__
Différents modèles d'héritage
Et bien d'autres encore, comme en témoigne cet excellent fil de discussion sur Reddit. Si vous souhaitez une explication plus détaillée des raisons pour lesquelles les classes de données ont été introduites et pourquoi elles ne remplacent pas directement les définitions de classes ordinaires, lisez le document PEP 557.
Si vous êtes intéressé par la programmation orientée objet en général, voici un cours qui vous permettra de poursuivre votre voyage :
Fondamentalement, les classes de données sont des structures plus sophistiquées qui permettent de conserver et d'extraire des données plus efficacement. Cependant, Python dispose de nombreuses autres structures de données qui effectuent cette tâche de manière plus ou moins similaire. Par exemple, vous pouvez en apprendre davantage sur les compteurs, defaultdicts et namedtuples dans le dernier chapitre du cours Data Types for Data Science.
Initiez-vous aux structures de données de Python : apprenez-en plus sur les types de données et les structures de données primitives et non primitives, telles que les chaînes de caractères, les listes, les piles, etc.
Découvrez les classes abstraites Python, leur utilité et comment utiliser le module `abc` pour garantir la cohérence des interfaces. Comprend des exemples pratiques et des bonnes pratiques pour une mise en œuvre efficace.
Tutoriel complet d'introduction aux boucles Python. Apprenez et pratiquez les boucles while et for, les boucles imbriquées, les mots-clés break et continue, la fonction range et bien plus encore.
Découvrez les fonctions et méthodes des listes Python. Veuillez suivre les exemples de code pour list() et d'autres fonctions et méthodes Python dès maintenant.
Découvrez le match-case de Python : un guide sur sa syntaxe, ses applications en data science, ML, et une analyse comparative avec le switch-case traditionnel.
Développez vos compétences en matière de données, découvrez comment manipuler et visualiser les données, et appliquez des analyses avancées pour prendre des décisions basées sur les données.
Les classes de données sont l'une des caractéristiques de Python qui, une fois que vous les avez découvertes, ne vous permet plus de revenir à l'ancienne méthode. Considérez cette classe régulière :
Pour moi, cette définition de classe est très inefficace - dans la méthode
__init__, vous répétez chaque paramètre au moins trois fois. Cela peut sembler anodin, mais pensez au nombre de fois où vous écrivez des classes avec beaucoup plus de paramètres.En comparaison, jetez un coup d'œil à l'alternative des classes de données du code ci-dessus :
Ce morceau de code d'apparence modeste est bien meilleur qu'une classe ordinaire. Le minuscule décorateur
@dataclassimplémente les classes__init__,__repr__,__eq__dans les coulisses, ce qui aurait nécessité au moins 20 lignes de code manuellement.En outre, de nombreuses autres fonctionnalités, telles que les opérateurs de comparaison, l'ordonnancement des objets et l'immutabilité, sont toutes à une ligne près d'être créées comme par magie pour notre classe.
Le but de ce tutoriel est donc de vous montrer pourquoi les classes de données sont l'une des meilleures choses qui soient arrivées à Python si vous aimez la programmation orientée objet.
Commençons !
Les bases des classes de données Python
Abordons quelques-uns des concepts fondamentaux des classes de données Python qui les rendent si utiles.
Certaines méthodes sont automatiquement générées dans les classes de données
Malgré toutes leurs caractéristiques, les classes de données sont des classes normales qui nécessitent beaucoup moins de code pour mettre en œuvre la même fonctionnalité. Voici à nouveau la classe
Exercise:À l'heure actuelle,
Exercisedispose déjà des méthodes__repr__et__eq__. Vérifions-le :La représentation d'un objet
reprdoit renvoyer le code qui peut se recréer lui-même, et nous pouvons voir que c'est exactement le cas pourex1.En comparaison,
Exercisedéfini selon l'ancienne méthode ressemblerait à ceci :Il a l'air assez horrible et inutile !
Vérifions maintenant l'existence de
__eq__, qui est l'opérateur d'égalité :La comparaison de la classe à elle-même et à une autre classe avec des paramètres identiques doit renvoyer True :
Et c'est le cas ! Dans les classes ordinaires, cette logique aurait été pénible à écrire.
Les classes de données nécessitent des indications de type
Comme vous l'avez peut-être remarqué, les classes de données nécessitent des indications de type lors de la définition des champs. En fait, les classes de données autorisent n'importe quel type du module
typing. Par exemple, voici comment créer un champ qui peut accepter le type de donnéesAny:Cependant, l'idiosyncrasie de Python est que même si les classes de données nécessitent des indications de type, les types ne sont pas réellement imposés.
Par exemple, la création d'une instance de la classe
Exerciseavec des types de données totalement incorrects peut être exécutée sans erreur :Si vous souhaitez renforcer les types de données, vous devez utiliser des vérificateurs de types tels que Mypy.
Les classes de données autorisent les valeurs par défaut dans les champs
Jusqu'à présent, nous n'avons pas ajouté de valeurs par défaut à nos classes. Corrigeons cela :
N'oubliez pas que les champs qui ne sont pas des champs par défaut ne peuvent pas suivre les champs par défaut. Par exemple, le code ci-dessous provoquera une erreur :
Dans la pratique, vous définirez rarement des valeurs par défaut à l'aide de la syntaxe
name: type = value.Vous utiliserez plutôt la fonction
field, qui permet de mieux contrôler chaque définition de champ :La fonction
fielda plus de paramètres, tels quereprinitcomparedefault_factoryet ainsi de suite. Nous les examinerons dans les sections suivantes.
Les classes de données peuvent être créées à l'aide d'une fonction
Une dernière remarque sur les bases des classes de données est que leur définition peut être encore plus courte en utilisant la fonction
make_dataclass:Mais vous sacrifierez la lisibilité, c'est pourquoi je ne recommande pas l'utilisation de cette fonction.
Classes de données avancées en Python
Dans cette section, nous aborderons les fonctionnalités avancées des classes de données qui apportent davantage d'avantages. L'une de ces caractéristiques est une usine par défaut.
Usines par défaut
Pour expliquer les usines par défaut, créons une autre classe nommée
WorkoutSessionqui accepte deux champs :En utilisant le type
List, nous spécifions queWorkoutSessionaccepte une liste d'instancesExercise.Actuellement, chaque instance de séance d'entraînement nécessite l'initialisation des exercices. Mais cela ne reflète pas la façon dont les gens s'entraînent - ils commencent d'abord une session (probablement dans une application), puis ils ajoutent des exercices au fur et à mesure qu'ils s'entraînent.
Il faut donc pouvoir créer des sessions sans exercices et sans durée. Pour ce faire, ajoutons une liste vide comme valeur par défaut pour
exercises:Cependant, nous avons obtenu une erreur - il s'avère que les classes de données n'autorisent pas les valeurs par défaut mutables.
Heureusement, il est possible de remédier à ce problème en utilisant une usine par défaut :
Le paramètre
default_factoryaccepte une fonction qui renvoie une valeur initiale pour un champ de la classe de données. Cela signifie qu'il peut accepter n'importe quelle fonction arbitraire :tupledictsetCeci est exact, que le résultat de la fonction soit mutable ou non.
Si l'on y réfléchit bien, la plupart des gens commencent leur entraînement par des exercices d'échauffement qui sont généralement similaires pour tout type de séance d'entraînement. Par conséquent, l'initialisation des sessions sans exercices peut ne pas correspondre à ce que certaines personnes souhaitent.
Au lieu de cela, créons une fonction qui renvoie trois sites d'échauffement
Exercises:Désormais, chaque fois que nous créons une session, les participants sont accompagnés d'exercices d'échauffement déjà enregistrés. La nouvelle version de
WorkoutSessiona une durée par défaut de cinq minutes pour en tenir compte.Ajouter des méthodes aux classes de données
Les classes de données étant des classes normales, l'ajout de méthodes reste le même. Ajoutons deux méthodes à notre classe de données
WorkoutSession:Grâce à ces méthodes, nous pouvons désormais enregistrer toute nouvelle activité dans une session :
Mais il y a un problème :
Lorsque nous imprimons la session, sa représentation par défaut est trop verbeuse et illisible puisqu'elle contient le code pour recréer l'objet. Corrigeons cela.
__repr__et__str__dans les classes de donnéesLes classes de données implémentent automatiquement
__repr__mais pas__str__. Cela permet à la classe de se rabattre sur__repr__lorsque nous faisons appel à print.Remplaçons donc ce comportement en définissant nous-mêmes
__str__:Le site
__repr__est toujours le même, mais lorsque nous appelonsprintsur ce site :La représentation printanière de la classe est beaucoup plus agréable. Maintenant, corrigeons également
WorkoutSession:Note: Utilisez le bouton "Expliquer le code" au bas de l'extrait pour obtenir une explication ligne par ligne du code.
Nous avons maintenant un résultat lisible et compact.
Comparaison des classes de données
Pour de nombreuses classes, il est judicieux de comparer leurs objets selon une certaine logique. Pour les séances d'entraînement, il peut s'agir de la durée de la séance, de l'intensité de l'exercice ou du poids.
Tout d'abord, voyons ce qui se passe si nous essayons de comparer deux séances d'entraînement dans l'état actuel :
Nous recevons
TypeErrorcar les classes de données n'implémentent pas d'opérateurs de comparaison. Mais cela peut être facilement corrigé en réglant le paramètreordersurTrue:Cette fois-ci, la comparaison fonctionne, mais qu'est-ce qu'on compare ?
Dans les classes de données, la comparaison est effectuée dans l'ordre dans lequel les champs sont définis. Actuellement, les classes sont comparées sur la base de la durée de l'entraînement car le premier champ,
exercises, contient des objets non standard.Nous pouvons le vérifier en augmentant la durée de la session de mercredi :
Comme prévu, nous avons reçu
False.Mais que se passerait-il si le premier champ de
Workoutétait un autre type de champ, par exemple une chaîne de caractères ? Essayons de le savoir :Même si la session du lundi dure plus longtemps, la comparaison montre qu'elle est plus petite que celle du mercredi. La raison en est que "25" vient avant "27" dans la comparaison de chaînes de Python.
Comment conserver l'ordre des champs tout en triant les sessions en fonction de la durée de la séance d'entraînement ? Cette opération est facile à réaliser grâce à la fonction
field:En définissant
comparesurFalsepour un champ quelconque, nous l'excluons du tri, comme le montre le résultat ci-dessus.Manipulation du champ post-init
À l'heure actuelle, la durée de la session est fixée par défaut à cinq minutes pour tenir compte des exercices d'échauffement. Toutefois, cela n'a de sens que si l'utilisateur commence une session par un échauffement. Que se passe-t-il s'ils commencent une session avec d'autres exercices ?
Pour un seul exercice, la durée totale est de cinq minutes, ce qui est illogique. Chaque session doit déterminer dynamiquement sa durée en fonction du nombre de séries de chaque exercice. Cela signifie que nous devons rendre
duration_minutesdépendant du champexercises.Mettons-le en œuvre :
Cette fois, nous définissons
duration_minutesavecinitfixé àFalsepour retarder l'initialisation du champ.Ensuite, à l'intérieur d'une méthode spéciale
__post_init__, nous mettons à jour sa valeur en fonction du nombre total d'ensembles dans chaqueExercise.Maintenant, lorsque nous initialisons
WorkoutSession, le siteduration_minutesest dynamiquement augmenté de trois minutes pour chaque série de chaque exercice.En général, si vous souhaitez définir un champ qui dépend d'autres champs de votre classe de données, vous pouvez utiliser la logique
__post_init__.L'immutabilité dans les classes de données
Notre classe de données
WorkoutSessionest presque prête ; il ne reste plus qu'à la protéger. À l'heure actuelle, il est assez facile d'y mettre le bazar :Nous voulons protéger tous les champs de nos classes afin qu'ils ne puissent être modifiés qu'à notre guise. Pour ce faire, le décorateur
@dataclasspropose un argument pratiquefrozen:Maintenant, si nous voulons modifier un champ, nous obtenons une erreur :
La définition de
frozenenTrueajoute automatiquement les méthodes__deleteattr__et__setattr__pour chaque champ afin qu'ils soient protégés contre la suppression ou les mises à jour après l'initialisation. En outre, les autres ne pourront pas non plus ajouter de nouveaux champs :Cette fonctionnalité comporterait des dizaines de lignes de code si nous avions affaire à des classes traditionnelles.
Notez toutefois que nous ne pouvons pas rendre nos classes réellement immuables. Par exemple, réécrivons le site
WorkoutSessionen remplaçantfrozenparTrue:Comme prévu, nous ne pouvons pas modifier directement la liste des exercices :
Cependant,
exercisesest une liste, qui est entièrement mutable, ce qui rend l'opération suivante possible :# Changement d'un des éléments d'une liste
Pour se prémunir contre les modifications accidentelles, il est donc recommandé d'utiliser des objets immuables, tels que des tuples, pour les valeurs des champs.
Héritage dans les classes de données
Un dernier point que nous aborderons est l'ordre des champs dans les classes parentales et enfantines.
Comme les classes de données sont des classes normales, l'héritage fonctionne comme d'habitude :
Mais comme le dernier champ de la classe mère (
ImmutableWorkoutSession) a une valeur par défaut, tous les champs des classes enfants doivent avoir des valeurs par défaut.Par exemple, ceci n'est pas autorisé :
Inconvénients des classes de données et autres ressources
Les classes de données ont été constamment améliorées depuis Python 3.7 (elles étaient excellentes au départ) et couvrent de nombreux cas d'utilisation où vous pourriez avoir besoin d'écrire des classes. Mais ils peuvent être désavantageux dans les scénarios suivants :
__init____new__Et bien d'autres encore, comme en témoigne cet excellent fil de discussion sur Reddit. Si vous souhaitez une explication plus détaillée des raisons pour lesquelles les classes de données ont été introduites et pourquoi elles ne remplacent pas directement les définitions de classes ordinaires, lisez le document PEP 557.
Si vous êtes intéressé par la programmation orientée objet en général, voici un cours qui vous permettra de poursuivre votre voyage :
Fondamentalement, les classes de données sont des structures plus sophistiquées qui permettent de conserver et d'extraire des données plus efficacement. Cependant, Python dispose de nombreuses autres structures de données qui effectuent cette tâche de manière plus ou moins similaire. Par exemple, vous pouvez en apprendre davantage sur les compteurs,
defaultdictsetnamedtuplesdans le dernier chapitre du cours Data Types for Data Science.