Il existe dans tout langage des éléments simples, pratiques, mais qui sont pourtant peu employés par les développeurs. En Python, les énumérations se retrouvent dans cette catégorie. Je vous propose dans cet article de découvrir leur intérêt.
La bibliothèque standard de Python contient une myriade de modules et il est difficile de tous les connaître. Pourtant, bon nombre d’entre eux peuvent se révéler très utiles ! Dans cet article, je vous propose d’étudier le module enum [1] qui permet de définir des énumérations. L’utilisation de ce module n’a rien de compliqué, mais encore faut-il le connaître pour penser à l’employer !
1. Une énumération simple
Commençons par créer une énumération simple. Pour cela, nous devons créer une classe qui hérite de la classe Enum et définir des attributs de classe qui correspondront à l’énumération. Ici, nous définirons les pièces d’un jeu d’échecs et nous afficherons différentes données pour comprendre le fonctionnement des énumérations :
L’énumération est simplement créée par la classe ChessPiece qui hérite d’Enum et définit les attributs de classe KING, QUEEN, etc. Par contre, bien entendu, une énumération est un peu plus complète qu’une classe comportant uniquement une suite d’attributs de classe :
- elle est itérable : l’affichage avec list() le prouve ;
- l’accès à un attribut de manière directe renvoie son nom ;
- il est possible d’accéder au nom d’un attribut en utilisant la propriété name et à sa valeur à l’aide de la propriété value.
Au lancement, nous obtenons :
Il faut noter que les valeurs attribuées aux pièces ne doivent pas nécessairement se suivre. Nous pouvons utiliser les valeurs réelles attribuées aux pièces d’un jeu d’échecs (avec le cavalier et le fou ayant la même valeur) :
Et bien entendu, les valeurs ne sont pas nécessairement des entiers. Voici un autre exemple cassant la logique du jeu d’échecs, mais illustrant que l’on peut vraiment faire ce que l’on souhaite avec les types :
Il est également possible de créer des énumérations en une ligne de manière fonctionnelle :
Le paramètre value définit le nom de la classe qui sera créée, names contient la liste des éléments de l’énumération sous forme d’une chaîne de caractères où les éléments sont séparés par des espaces (on peut aussi employer un tuple names=('KING', 'QUEEN', ...)), et start demande à ce que les valeurs associées aux éléments soient des entiers qui débutent par 1 et incrémentés automatiquement.
Il est également possible d’associer manuellement des valeurs aux éléments en utilisant un dictionnaire :
2. Opérations sur les énumérations
Nous distinguerons deux types d’opérations différents : l’itération, dont nous venons de tester un effet en utilisant la fonction list(), et la comparaison d’énumérations.
2.1 Itération
Il est possible de parcourir une énumération pour en afficher tous les éléments et leurs valeurs :
Nous obtenons :
Attention !
Vous remarquerez dans la sortie l’absence de l’élément KNIGHT : celui-ci a la même valeur que BISHOP et un seul élément est affiché par valeur (le premier). D’ailleurs, après la définition du premier élément, tous les autres de même valeur seront en quelque sorte des pointeurs vers le premier :
Pour les trois pièces, le nom affiché sera le même (mais les éléments existent bel et bien) :
Vous pouvez tout de même vérifier l’existence des autres éléments à l’aide du code suivant :
Vous les verrez bien apparaître avec l’élément auquel ils sont « associés » :
2.2 Comparaison
Dans une énumération, les éléments ne sont pas ordonnés et donc la seule comparaison possible est une comparaison d’égalité (ou d’identité) :
La condition king == ChessPiece.QUEEN peut être transformée en king is ChessPiece.QUEEN pour une relation d’identité.
3. Unicité des valeurs
Nous savons que l’on pouvait associer la même valeur à plusieurs éléments (cas de BISHOP et KNIGHT avec la valeur 3). Pour forcer l’unicité des valeurs, il faut utiliser le décorateur Enum.unique :
L’exécution de ce script provoquera une erreur, l’exception levée nous indiquant de manière détaillée la source du problème :
4. Valeurs automatiques
Tout comme il est possible d’allouer automatiquement des valeurs aux éléments avec la création sur une ligne (paramètre start), il est possible de demander à ce que les éléments se voient associer une valeur entière s’incrémentant à chaque nouvelle définition grâce à la classe auto :
Les valeurs des éléments se suivront alors en commençant par 1 :
L’insertion de valeurs non automatiques est toujours possible :
Le premier élément prendra la valeur 1, le second la valeur 2, etc. jusqu’à l’élément qui est une chaîne de caractères. À l’appel suivant de auto(), les valeurs reprendront là où elles avaient été arrêtées :
Si vous ne souhaitez pas que auto() génère des valeurs entières de 1 en 1, vous pouvez tout à fait modifier ce comportement en surchargeant la méthode _generate_next_value_():
Ici, tous les éléments se verront affecter une lettre (dans la limite de la disponibilité dans la table ASCII) :
5. Gestion de drapeaux
Pour terminer ce tour d’horizon des énumérations, voici maintenant une classe fille de Enum, la classe Flag, qui permet de gérer des drapeaux en autorisant la création d’ensembles d’éléments avec l’opérateur de bit | :
Ici, nous associons à fileAccess les permissions READ et WRITE. Pour tester si fileAccess possède l’une ou l’autre permission, il faut employer la structure <permission> in <variable>. Pour une correspondance de toutes les permissions, il est possible d’utiliser l’opérateur d’identité is. Notez que l’ordre dans lequel les permissions ont été définies et celui dans lequel elles sont testées importent peu.
6. Exemples d’application
Maintenant que nous avons vu ce qu’il était possible de réaliser avec les énumérations d’un point de vue syntaxique, il serait intéressant de savoir dans quel cas il pourrait être utile de les utiliser...
6.1 Gestion de couleurs
En ajoutant des méthodes à la classe dérivant de Enum, nous pouvons définir une classe stockant les codes couleur et permettant de modifier la couleur d’affichage d’un texte en mode console :
6.2 Tester des valeurs de retour de manière lisible
Supposons que vous ayez besoin d’effectuer des requêtes sur le Web et donc de tester si tout s’est correctement déroulé. En utilisant le module Requests [2], la valeur de retour d’une requête est fournie dans l’attribut status_code. Il suffit donc de tester ce dernier :
Je n’ai utilisé ici que deux codes d’erreur (en plus très connus), mais reconnaissez que le code n’est pas particulièrement lisible. Imaginez le cas où l’erreur serait 405 ou encore 306... Il vous faudrait vous plonger dans la documentation pour retrouver la signification du code d’erreur ! Alors qu’avec des énumérations, ce serait si simple :
Conclusion
Les énumérations apportent essentiellement de la lisibilité dans les codes. Elles sont peu employées, pourtant en termes de maintenabilité de code, elles permettent de gagner énormément de temps. Pensez-y la prochaine fois que vous écrirez un script...
Références
[1] Module Enum : https://docs.python.org/fr/3/library/enum.html
[2] Module Requests : https://requests.readthedocs.io/en/master/