Pierre Gradot
Posted on March 26, 2020
Une des fonctionnalités principales ajoutées en Python 3.7 est le module dataclasses
, décrit dans la PEP557, et qui contient notamment l'annotation @dataclass
pour créer des data classes.
L'abstract de cette PEP nous donne l'utilité de cette fonctionnalité :
This PEP describes an addition to the standard library called Data Classes. Although they use a very different mechanism, Data Classes can be thought of as "mutable namedtuples with defaults". Because Data Classes use normal class definition syntax, you are free to use inheritance, metaclasses, docstrings, user-defined methods, class factories, and other Python class features.
Vous ne vous servez pas de namedtuple()
aujourd'hui et du coup l'utilité vous échappe ? OK, commençons par ça !
Retour sur namedtuple()
Quand vous faites du code, vous avez souvent besoin d'objets qui servent uniquement à stocker des données, et qui n'ont pas de méthode car pas de comportement associé. Prenons par exemple une transaction entre deux entités : on souhaite stocker les identifiants des entités et le contenu de la transaction. Il y a plusieurs solutions en Python pour implémenter un tel objet : classe, dictionnaire, tuple...
La solution la plus évidente quand on vient du monde de la POO est de faire une classe :
class Transaction:
def __init__(self, source, destination, content):
self.source = source
self.destination = destination
self.content = content
Ca fait beaucoup de code pour juste décrire des champs... Mais il y a d'autres trucs dommages :
>>> transaction = Transaction('A', 'B', 'hello there!')
>>> print(transaction)
<__main__.Transaction object at 0x00000218590A0520>
>>> t1 = Transaction('A', 'B', 'same')
>>> t2 = Transaction('A', 'B', 'same')
>>> t1 == t2
False
>>> t3 = t1
>>> t1 == t3
True
On constate que :
- Il n'y a pas d'implémentation automatique des méthodes
__str__(self)
ou__repr__(self)
. - La comparaison par défaut ne se fait pas sur les contenus mais sur les adresses des objets, ce qui n'est pas intuitif (même si c'est le comportement normal en Python).
Depuis Python 2.4, le module collections
propose une aide pour créer de tels objets : les named tuples (ou tuples nommés, en français). Le module contient en effet la fonction namedtuples()
qui est une factory pour créer des types. Ces types sont des tuples dont les éléments sont nommés. Voici par exemple comment créer un type équivalent au précédent :
from collections import namedtuple
Transaction = namedtuple('Transaction', 'source, destination, content')
C'est beaucoup plus compact et que les trois points cités précédemment sont réglés :
>>> transaction = Transaction('A', 'B', 'hello there!')
>>> print(transaction)
Transaction(source='A', destination='B', content='hello there!')
>>> t1 = Transaction('A', 'B', 'same')
>>> t2 = Transaction('A', 'B', 'same')
>>> t1 == t2
True
Les named tuples ne sont pas parfaits non plus, et c'est pour ça que les data classes ont été imaginées. La PEP557 l'explique en détails. Le défaut le plus criant est peut-être que leur typage est faible et que comparer deux named tuples revient à comparer leurs contenus mais pas leurs types :
from collections import namedtuple
Transaction = namedtuple('Transaction', 'source, destination, content')
Person = namedtuple('Person', 'name, firstname, city')
transaction = Transaction('Pierre-Marie', 'Carole', 'Nantes')
person = Person('Pierre-Marie', 'Carole', 'Nantes')
print(transaction == person) # --> affiche 'True' alors que le types sont différents !
Un autre défaut est que les named tuples sont forcément immutables, il est donc impossible de modifier leurs champs. Voici un code :
from collections import namedtuple
Person = namedtuple('Person', 'name, firstname, city')
pierre = Person('Gradot', 'Pierre', 'Rennes')
pierre.city = 'Nantes'
Et voici l'erreur qu'il génère :
Traceback (most recent call last):
File "C:/Users/z19100018/Desktop/temp/temp.py", line 4, in <module>
pierre.city = 'Nantes'
AttributeError: can't set attribute
@dataclass en action !
Voyons ce que @dataclass
nous permet de faire. Créeons des classes avec cette annotation :
from dataclasses import dataclass
@dataclass(frozen=True)
class Transaction:
source: str
destination: str
content: str
@dataclass(frozen=False)
class Person:
name: str
firstname: str
city: str
Et utilisons-les :
>>> transaction = Transaction('A', 'B', 'hello there!')
>>> print(transaction)
Transaction(source='A', destination='B', content='hello there!')
>>> t1 = Transaction('A', 'B', 'same')
>>> t2 = Transaction('A', 'B', 'same')
>>> t1 == t2
True
On retrouve les avantages de named tuples mais on gomme aussi son gros inconvénient :
>>> transaction = Transaction('Pierre-Marie', 'Carole', 'Nantes')
>>> person = Person('Pierre-Marie', 'Carole', 'Nantes')
>>> transaction == person
False # --> c'est beaucoup mieux !
Vous constatez qu'on peut préciser au décorateur, grâce à son paramètre frozen
si les instance sont mutables ou pas :
>>> transaction = Transaction('A', 'B', 'hello there!')
>>> transaction.message = 'another message'
Traceback (most recent call last):
File "<pyshell#19>", line 1, in <module>
transaction.message = 'another message'
File "<string>", line 3, in __setattr__
dataclasses.FrozenInstanceError: cannot assign to field 'message'
>>> pierre = Person('Gradot', 'Pierre', 'Rennes')
>>> print(pierre)
Person(name='Gradot', firstname='Pierre', city='Rennes')
>>> pierre.city = 'Nantes'
>>> print(pierre)
Person(name='Gradot', firstname='Pierre', city='Nantes')
Un dernier point avantage qui m'a convaincu personnellement d'abandonner les named tuples au profit des data classes est qu'on retrouve la souplesse d'une classe pour ajouter des méthodes. J'ai effet commencé cet article en disant que ce genre de classe n'avait pas pour vocation d'apporter du comportement, mais on finit souvent par avoir envie (ou besoin !) d'en rajouter.
@dataclass(frozen=True)
class Transaction:
source: str
destination: str
content: str
def is_valid(self):
return self.source != "" and self.destination != "" \
and self.source != self.destination
t1 = Transaction('A', 'B', 'this is fine')
t1.is_valid() # --> renvoie True
Conclusion
Les data classes sont une nouvelle alternative pour créer facilement des classes dont le but premier est de stocker des données. Elles n'ont pas vocation à remplacer les named tuples mais dans de nombreuses situations elles peuvent s'avérer bien plus souples et fiables.
Une data class est une classe sur laquelle on applique le décorateur @dataclass
. Vous spécifiez les attributs de votre classe et le décorateur génère pour vous plusieurs méthodes comme :
- un constructeur pour initialiser tous ces champs
- une implémentation élégante de
__repr(self)__
- des méthodes de comparaisons
Merci d'avoir lu cet article !
Il a été posté initialement sur notre blog : https://www.younup.fr/blog/
Posted on March 26, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.