Modéliser une donnée en Python avec le namedtuple

Une difficulté pour écrire un bon programme est de représenter correctement les données. En effet, une donnée est en générale complexe, j’entends pas là qu’elle est représentée par plusieurs données primitives. Ce n’est alors pas la donnée en elle-même qui nous intéressera mais un de ses composants.

Je vais prendre pour illustration l’article sur les explosions dans les jeux vidéos où une explosion est un ensemble de particules. et où une particule consiste en une coordonnée (x et y), une vélocité représentée par la vélocité sur l’axe des abscisses (vx) et sur l’axe des ordonnées (vy) et un âge. Si une explosion n’est qu’une séquence de particules, une particule est un ensemble d’informations auxquelles il faut accéder de manière spécifique.

Liste, n-uplet (tuple) ou dictionnaire sont les premières représentations auxquelles on peut penser surtout lorsqu’on débute en programmation. Puis, lorsqu’on a appris l’objet, on bascule vers les classes afin de simplifier la syntaxe.

En Python, nous avons d’autres structures de données et cet article va vous présenter le namedtuple ou n-uplets avec des champs nommés.

Représentation de notre donnée, la particule

Les auteurs de l’article du Wireframe Mag ont représenté chaque particule sous forme d’un n-uplet (tuple)

particle = (x, y, vx, vy, age)

Utiliser un n-uplet est cohérent mais peu pratique. En effet, pour accéder à chaque valeur, il faut utiliser la position de la valeur au sein du n-uplet. Avec ce modèle, il y a donc un risque d’erreurs et une limite à l’évolution de cette donnée.

Lorsqu’un développeur utilise un langage Objet, le réflexe est de se trouver vers les objets et de définir une classe. Ainsi, la syntaxe d’accès aux attributs limitera les risques d’erreur et l’encapsulation permettra de faire évoluer le modèle si besoin.

Cependant, c’est souvent un mauvais choix car ces objets ne le sont pas d’un point de vue Programmation Orientée Objet (POO). En effet, selon les principes de la POO, un objet doit avoir certes des données mais aussi des comportements. On se retrouve souvent dans ces cas là à avoir des classes sans comportement.

En Python, nous allons pouvoir utiliser un type de données intermédiaire, plus adapté : les namedtuple ou n-uplets avec des champs nommés.

Représentation avec un namedtuple

Le type namedtuple est proposé par la bibliothèque collections qui fait parti de la distribution standard de Python. Il faudra donc l’importer avant de l’utiliser.

from collections import namedtuple

Il s’agit en fait d’une fonction qui va nous permettre de créer ces fameux namedtuple qui en fait n’existeront jamais dans votre programme. Pour comprendre, voyons comment s’utilise cette fonction avec notre exemple.

L’usage le plus simple de cette fonction va être en fournissant deux arguments : le nom du type que nous voulons créer sous forme de chaine de caractères et une séquence de chaines de caractères représentant le noms d’attributs. La fonction retournera un type qu’il est d’usage de référencer dans une variable du même nom.

Particle = namedtuple("Particle", ("x", "y", "vx", "vy", "age"))

Nous venons donc de créer un type Particule qui est un callable qui prends 5 paramètres.

p = Particle(12, 45, 5, 3, 0)

L’objet retourné est de type Particle mais aussi de type tuple. Nous allons donc pouvoir l’utiliser comme un n-uplet en accédant à ses éléments par indice (lignes 1 et 2) ou par déballage (unpacking, ligne 4).

x = p[0]
y = p[1]

x, y, vx, vy, age = p

Mais nous pouvons aussi avec ce type utiliser la syntaxe objet d’accès aux attributs.

x = p.x
y = p.y
age = p.age

La fonction namedtuple permet donc de créer des types afin de créer des objets qui seront également de type tuple, objets pour lesquels on accède aux éléments par le nom de leur champ. Ces objets restent des n-uplets et sont donc immuables (vous ne pouvez pas remplacer une valeur même par affectation de champ).

Nous avons donc un intermédiaire entre le n-uplet et l’objet. Les programmes avec des données métier sous forme de namedtuples seront donc plus simples à écrire et lire et ne poseront pas de problème d’évolution comme avec l’usage de listes et de dictionnaires et respectent le paradigme Objet.

Gérer les paramètres optionnels

La création de certaines données doit prendre en charge la notion de paramètres optionnels. Après tout, dans notre exemple, l’argument age est à 0 lors de la création de la particule.

Le namedtupe le permet en prenant en argument (optionnel) une séquence de valeurs par défaut. Dans notre cas, ce sera :

Particle = namedtuple("Particle", ("x", "y", "vx", "vy", "age"),
                      defaults=(0,))

Comme les paramètres optionnels doivent suivre les paramètres positionnels (sans valeur par défaut), les éléments de cet itérable seront affectés aux derniers éléments de la liste d’attributs, dans l’ordre. Dans notre cas, 0 est affecté à age.

À titre d’exemple, si nous considérons que par défaut la vélocité verticale doit être de 2 et l’horizontale de 1, ce code serait :

Particle = namedtuple("Particle", ("x", "y", "vx", "vy", "age"),
                      defaults=(2, 1, 0,))

À nouveau, ces valeurs sont optionnelles, elles viennent compléter si nécessaire les attributs manquants lors de la création du namedtuple.

Des méthodes spécifiques

Le namedtuple implémente 3 méthodes dédiées à sa manipulation. Celles-ci sont préfixées par un tiret bas (underscore) comme les méthodes non-publiques d’après la documentation pour éviter un conflit de noms.

Pour commencer, la méthode namedtuple._make(iterable) est une méthode de classe qui permet de créer un objet à partir d’un itérable.

particle_data = [45, 67, 3, 4, 1]
p = Particle._make(particle_data)

Elle est destinée à être utilisée pour la création de namedtuples lors de la lecture de données tel que les fichiers csv ou les retours de bases de données.

La méthode namedtuple._asdict() retourne simplement le contenu de l’objet sous forme d’un dictionnaire où les clefs seront les noms des champs associés à leurs valeurs respectives.

>>> p = Particle(45, 67, 3, 4, 1)
>>> p._asdict()
{'x': 45, 'y': 67, 'vx': 3, 'vy': 4, 'age': 1}

Depuis Python 3.8, le dictionnaire natif de Python garanti la conservation de l’ordre d’ajout des éléments ce que nous retrouvons donc ici. De Python 3.1 et 3.7, cette méthode retourne un OrderedDict.

Enfin, namedtuple._replace(**kwargs) permet de remplacer la valeur d’un ou de plusieurs champs. Le namedtuple est un n-uplet, donc, immuable. Cette méthode va donc évidemment retourner une nouvelle instance avec les données à jour.

>>> p._replace(x=p.x + p.vx, y=p.y + p.vy)
Particle(x=48, y=71, vx=3, vy=4, age=1)
>>> print(p)
Particle(x=45, y=67, vx=3, vy=4, age=1)

Et des attributs spécifiques…

Le type namedtuple possède également deux attributs de classe utiles pour l’introspection. Pour commencer, namedtuple._fields retourne un n-uplet des noms des champs sous forme de chaines de caractères. Ensuite, namedtuple._field_defaults retourne un dictionnaire où les clefs sont les noms des champs ayant une valeur par défaut et la valeur, la valeur par défaut qui leur est associée. Leurs appels possibles sont présentés ci-dessous.

>>> Particle = namedtuple("Particle", ("x", "y", "vx", "vy", "age"),
                          defaults=(2, 1, 0,))
>>> p = Particle(10,25)
>>> p._fields
('x', 'y', 'vx', 'vy', 'age')
>>> p._field_defaults
{'vx': 2, 'vy': 1, 'age': 0}

>>> Particle._fields
('x', 'y', 'vx', 'vy', 'age')
>>> Particle._field_defaults
{'vx': 2, 'vy': 1, 'age': 0}

Un aspect intéressant de l’attribut _fields est qu’il peut permettre de construire d’autres namedtuples plus complexes sur la base de namedtuples plus simples. Par exemple :

>>> Point = namedtuple("Point", ("x", "y"))
>>> VectorialSpeed = namedtuple('VectorialSpeed', ("vx", "vy"))

>>> Particle = namedtuple("Particle",
                          Point._fields + VectorialSpeed._fields 
                          + ("age",))
>>> Particle._fields
('x', 'y', 'vx', 'vy', 'age')

Les limites des namedtuple

Un namedtuple reste une séquence, un simple transporteur de données. Lorsque vous instanciez un namedtuple, il n’y a aucune validation ou transformation des données comme vous pourriez en avoir dans un constructeur. Il n’y a pas non plus de possibilité de donner les capacités spéciales (avec les méthodes spéciales comme les comparaisons) à ces types.

Et ce n’est pas grave… Le namedtuple n’est pas destiné à ça, c’est bien un type destiné à transporter des données sans plus, la manipulation de ces données étant à la charge d’autres composants.

L’évolution vers l’objet

Les besoins des programmes évoluent. Il est possible qu’à un moment, vous arriviez à la limite du namedtuple et qu’il sera nécessaire de passer à l’Objet en définissant une classe.

Et bien vous pouvez voir qu’il n’y aura pas trop de conséquences sur le code existant. Il suffit de remplacer la définition du type par une classe. Si vous n’utilisez aucune méthode spécifique du namedtuple, c’est tout. Grâce au Duck Typing, le code qui utilise le namedtuple n’a besoin d’aucune modification. Tous les accès aux attributs seront identique et vous pourrez ajouter les fonctionnalités de l’Objet.

En conclusion

Le namedtuple est un type de donnée adaptée à la représentation de donnée complexe. Il est suffisamment souple pour vous permettre de représenter votre information et de l’utiliser avec la syntaxe d’accès aux attributs. Le fait que ce soit un n-uplet, donc immuable, est justement bien adapté à la notion transport de données.

Néanmoins, si votre besoin évolue et que vous avez besoin de vous tourner vers l’Objet, la syntaxe et le duck-typing permet de faire l’évolution avec le minimum d’impact sur le code existant.

Si vous avez aimé ce post, n’hésitez pas à laisser un commentaire ci-dessous ou sur la page Facebook 😉

À propos de... Darko Stankovski

iT guy, photographe et papa 3.0, je vous fais partager mon expérience et découvertes dans ces domaines. Vous pouvez me suivre sur les liens ci-dessous.

Vous aimerez aussi...

2 réponses

  1. 25 avril 2023

    […] champs de la séquence, l’écriture par indice peut-être pénible. On peut alors songer aux namedtuple présentés dans l’article précédent. Mais qu’en est-il de la performance ? Ce sera le sujet d’un article […]

  2. 27 avril 2023

    […] données. En Python, pour respecter les principes de la programmation orientée objet, nous avons les namedtuples que nous avons vu dans un article précédent. Mais ceux-ci ne permettent pas d’ajouter un comportement. Les classes restent donc la […]

Laisser un commentaire