<
Media
>
Article

Tout ce que vous devez savoir sur le module dataclasses de Python 3.7

7 min
03
/
12
/
2019

Une des fonctionnalités principales ajoutées en Python 3.7 est le module <span class="css-span">dataclasses</span>, décrit dans la PEP557, et qui contient notamment l'annotation <span class="css-span">@dataclass</span> 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 !

logo python

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 :

<pre><code>class Transaction:
   def __init__(self, source, destination, content):
       self.source = source
       self.destination = destination
       self.content = content</code></pre>

Ca fait beaucoup de code pour juste décrire des champs... Mais il y a d'autres trucs dommages :

<pre><code>>>> 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</code></pre>

On constate que :

  • Il n'y a pas d'implémentation automatique des méthodes<span class="css-span"> __str__(self)</span> ou <span class="css-span">__repr__(self)</span>.
  • 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 :

<pre><code>from collections import namedtuple

Transaction = namedtuple('Transaction', 'source, destination, content')</code></pre>

C'est beaucoup plus compact et que les trois points cités précédemment sont réglés :

<pre><code>>>> 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</code></pre>

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 :

<pre><code>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 !</code></pre>

Un autre défaut est que les named tuples sont forcément immutables, il est donc impossible de modifier leurs champs. Voici un code :

<pre><code>from collections import namedtuple
Person = namedtuple('Person', 'name, firstname, city')
pierre = Person('Gradot', 'Pierre', 'Rennes')
pierre.city = 'Nantes'</code></pre>

Et voici l'erreur qu'il génère :

<pre><code>Traceback (most recent call last):
 File "C:/Users/z19100018/Desktop/temp/temp.py", line 4, in &lt;module>
   pierre.city = 'Nantes'
AttributeError: can't set attribute</code></pre>

@dataclass en action !

Voyons ce que <span class="css-span">@dataclass</span> nous permet de faire. Créons des classes avec cette annotation :

<pre><code>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</code></pre>

Et utilisons-les :

<pre><code>>>> 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</code></pre>

On retrouve les avantages de named tuples mais on gomme aussi son gros inconvénient :

<pre><code>>>> transaction = Transaction('Pierre-Marie', 'Carole', 'Nantes')
>>> person = Person('Pierre-Marie', 'Carole', 'Nantes')
>>> transaction == person
False # --> c'est beaucoup mieux !</code></pre>

Vous constatez qu'on peut préciser au décorateur, grâce à son paramètre <span class="css-span">frozen</span> si les instances sont mutables ou non :

<pre><code>>>> 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')</code></pre>

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.

<pre><code>@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</code></pre>


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 <span class="css-span">__repr(self)__</span>
  • des méthodes de comparaisons
No items found.
ça t’a plu ?
Partage ce contenu
Pierre

Que la vie de Pierre, expert embarqué Younup, serait terne sans les variadic templates et les fold expressions de C++17. Heureusement pour lui, Python a tué l'éternel débat sur l’emplacement de l’accolade : "alors, à la fin de la ligne courante ou au début de la ligne suivante ?"

Homme de terrain, il est aussi à l’aise au guidon de son VTT à sillonner les chemins de forêt, dans une salle de concert de black metal ou les mains dans les soudures de sa carte électronique quand il doit déboguer du code (bon ça, il aime moins quand même !)

Son vœu pieux ? Il hésite encore... Faire disparaitre le C embarqué au profit du C++ embarqué ? Ou stopper la génération sans fin d'entropie de son bureau.