Utilisez les énumérations en Python

Magazine
Marque
GNU/Linux Magazine
HS n°
Numéro
115
Mois de parution
juillet 2021
Spécialité(s)


Résumé

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.


Body

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 :

from enum import Enum
 
 
class ChessPiece(Enum):
    KING : int = 1
    QUEEN : int = 2
    ROOK : int = 3
    BISHOP : int = 4
    KNIGHT : int = 5
    PAWN : int = 6
 
 
if __name__ == '__main__':
    print(list(ChessPiece))
 
    print(ChessPiece.KING)
    print(f'{ChessPiece.KING.name=}')
    print(f'{ChessPiece.KING.value=}')

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 :

[<ChessPiece.KING: 1>, <ChessPiece.QUEEN: 2>, <ChessPiece.ROOK: 3>, <ChessPiece.BISHOP: 4>, <ChessPiece.KNIGHT: 5>, <ChessPiece.PAWN: 6>]
ChessPiece.KING
ChessPiece.KING.name='KING'
ChessPiece.KING.value=1

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) :

...
class ChessPiece(Enum):
    KING : int = 100
    QUEEN : int = 9
    ROOK : int = 5
    BISHOP : int = 3
    KNIGHT : int = 3
    PAWN : int = 1
...

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 :

...
class ChessPiece(Enum):
    KING : int = 'infini !'
    QUEEN : int = 9
    ROOK : int = (5, None, None)
    BISHOP : int = [3, None]
    KNIGHT : int = {'life' : 3, 'power' : 5}
    PAWN : int = 1
...

Il est également possible de créer des énumérations en une ligne de manière fonctionnelle :

ChessPiece = Enum(
    value='ChessPiece',
    names=('KING QUEEN ROOK BISHOP KNIGHT PAWN'),
    start=1
)

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 :

ChessPiece = Enum(
    value='ChessPiece',
    names={
        'KING' : 100,
        'QUEEN' : 9,
        'ROOK' : 5,
        'BISHOP' : 3,
        'KNIGHT' : 3,
        'PAWN' : 1
    }
)

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 :

...
if __name__ == '__main__':
    for elt in ChessPiece:
        print(f'{elt.name:7} : {elt.value}')

Nous obtenons :

KING    : 100
QUEEN   : 9
ROOK    : 5
BISHOP : 3
PAWN    : 1

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 :

from enum import Enum
 
 
class ChessPiece(Enum):
    KING : int = 100
    QUEEN : int = 9
    ROOK : int = 5
    BISHOP : int = 3
    KNIGHT : int = 3
    OTHER : int = 3
    PAWN : int = 1
 
 
if __name__ == '__main__':
    print(f'{ChessPiece.BISHOP.name=}')
    print(f'{ChessPiece.KNIGHT.name=}')
    print(f'{ChessPiece.OTHER.name=}')

Pour les trois pièces, le nom affiché sera le même (mais les éléments existent bel et bien) :

ChessPiece.BISHOP.name='BISHOP'
ChessPiece.KNIGHT.name='BISHOP'
ChessPiece.OTHER.name='BISHOP'

Vous pouvez tout de même vérifier l’existence des autres éléments à l’aide du code suivant :

for name, member in ChessPiece.__members__.items():
    print(name, member)

Vous les verrez bien apparaître avec l’élément auquel ils sont « associés » :

KING ChessPiece.KING
QUEEN ChessPiece.QUEEN
ROOK ChessPiece.ROOK
BISHOP ChessPiece.BISHOP
KNIGHT ChessPiece.BISHOP
OTHER ChessPiece.BISHOP
PAWN ChessPiece.PAWN

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é) :

if __name__ == '__main__':
    king = ChessPiece.KING
 
    if king == ChessPiece.QUEEN:
        print('There is a big problem !')

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 :

from enum import Enum, unique
 
 
@unique
class ChessPiece(Enum):
    ...
    BISHOP : int = 3
    KNIGHT : int = 3
    OTHER : int = 3
    PAWN : int = 1
 
 
if __name__ == '__main__':
    print(f'{ChessPiece.BISHOP.name=}')
    print(f'{ChessPiece.KNIGHT.name=}')
    print(f'{ChessPiece.OTHER.name=}')

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 :

Traceback (most recent call last):
  File "/home/login/dev/ex_1.py", line 5, in <module>
    class ChessPiece(Enum):
  File "/usr/local/lib/python3.9/enum.py", line 884, in unique
    raise ValueError('duplicate values found in %r: %s' %
ValueError: duplicate values found in <enum 'ChessPiece'>: KNIGHT -> BISHOP, OTHER -> BISHOP

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 :

from enum import Enum, auto
 
 
class ChessPiece(Enum):
    KING : int = auto()
    ...
    PAWN : int = auto()
 
 
if __name__ == '__main__':
    for elt in ChessPiece:
        print(f'{elt.name:7} : {elt.value}')

Les valeurs des éléments se suivront alors en commençant par 1 :

KING    : 1
QUEEN   : 2
...
PAWN    : 5

L’insertion de valeurs non automatiques est toujours possible :

...
class ChessPiece(Enum):
    KING : int = auto()
    ...
    BISHOP : str = 'chaîne de caractères'
    KNIGHT : int = auto()
    PAWN : int = auto()

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 :

KING    : 1
QUEEN   : 2
ROOK    : 3
BISHOP : chaîne de caractères
KNIGHT : 4
PAWN    : 5

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_():

from enum import Enum, auto
 
 
class AutoChessPiece(Enum):
        def _generate_next_value_(name, start, count, last_values):
            if len(last_values) > 0:
                return chr(ord(last_values[-1]) + 1)
            return 'A'
 
 
class ChessPiece(AutoChessPiece):
    KING : str = auto()
    QUEEN : str = auto()
    ROOK : str = auto()
    BISHOP : str = auto()
    KNIGHT : str = auto()
    PAWN : str = auto()
 
 
if __name__ == '__main__':
    for elt in ChessPiece:
        print(f'{elt.name:7} : {elt.value}')

Ici, tous les éléments se verront affecter une lettre (dans la limite de la disponibilité dans la table ASCII) :

KING    : A
QUEEN   : B
ROOK    : C
BISHOP : D
KNIGHT : E
PAWN    : F

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 | :

from enum import Flag, auto
 
 
class FilePermission(Flag):
    READ : int = auto()
    WRITE : int = auto()
    EXECUTE : int = auto()
 
 
if __name__ == '__main__':
    fileAccess = FilePermission.READ | FilePermission.WRITE
 
    if FilePermission.READ in fileAccess:
        print('Accès en lecture')
    if FilePermission.WRITE in fileAccess:
        print('Accès en écriture')
    if FilePermission.EXECUTE in fileAccess:
        print('Accès en exécution')
    if fileAccess is FilePermission.WRITE | FilePermission.READ:
        print('Accès en écriture et en lecture')

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 :

from enum import Enum
 
 
class Color(Enum):
    RED : int = 31
    GREEN : int = 32
    YELLOW : int = 33
 
    def colorize(self) -> None:
        print(f'\033[{self.value}m')
 
    def __str__(self) -> str:
        return f'{self.name} : #{self.value}'
 
 
if __name__ == '__main__':
    Color.RED.colorize()
    print('GLMF')

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 :

import requests
 
 
if __name__ == '__main__':
    page = requests.get('http://www.google.fr')
    if page.status_code == 404:
        print('Erreur lors du chargement de la page')
    elif page.status_code == 200:
        print('OK, on peut continuer...')

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 :

from enum import Enum
import requests
 
 
class HTTPCode(Enum):
    SUCCES : int = 200
    BAD_REQUEST : int = 400
    ACCESS_DENIED : int = 403
    NOT_FOUND : int = 404
    METHOD_NOT_ALLOWED : int = 405
 
 
if __name__ == '__main__':
    page = requests.get('http://www.google.fr')
    if page.status_code is HTTPCode.NOT_FOUND:
        print('Erreur lors du chargement de la page')
    elif page.status_code is HTTPCode.SUCCES:
        print('OK, on peut continuer...')

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/



Article rédigé par

Par le(s) même(s) auteur(s)

Contrôler un serveur avec des SMS

Magazine
Marque
GNU/Linux Magazine
HS n°
Numéro
118
Mois de parution
février 2022
Spécialité(s)
Résumé

Utiliser des SMS pour communiquer avec un serveur peut paraître assez loufoque à notre époque. Pourtant, cela peut être très utile quand l’utilisateur final n’est pas un informaticien et que l’on ne souhaite pas nécessairement développer une application spécifique.

Édito

Magazine
Marque
GNU/Linux Magazine
Numéro
255
Mois de parution
janvier 2022
Résumé

Dans des temps anciens, les logiciels propriétaires et les logiciels open source se menaient une guerre sévère. Ces temps-là sont désormais révolus. On ne peut pas dire que l’un ou l’autre bord ait gagné, mais en tout cas, il n’existe plus de tension aussi forte entre les partisans des deux camps. On peut se dire que c’est l’open source qui a gagné, qui a finalement été accepté. Mais c’est sans doute oublier un peu vite que l’on peut établir une distinction entre logiciel open source et logiciel libre, le premier profitant de la philosophie du second à des fins purement pécuniaires.

Les derniers articles Premiums

Les derniers articles Premium

Bénéficiez de statistiques de fréquentations web légères et respectueuses avec Plausible Analytics

Magazine
Marque
Contenu Premium
Spécialité(s)
Résumé

Pour être visible sur le Web, un site est indispensable, cela va de soi. Mais il est impossible d’en évaluer le succès, ni celui de ses améliorations, sans établir de statistiques de fréquentation : combien de visiteurs ? Combien de pages consultées ? Quel temps passé ? Comment savoir si le nouveau design plaît réellement ? Autant de questions auxquelles Plausible se propose de répondre.

Quarkus : applications Java pour conteneurs

Magazine
Marque
Contenu Premium
Spécialité(s)
Résumé

Initié par Red Hat, il y a quelques années le projet Quarkus a pris son envol et en est désormais à sa troisième version majeure. Il propose un cadre d’exécution pour une application de Java radicalement différente, où son exécution ultra optimisée en fait un parfait candidat pour le déploiement sur des conteneurs tels que ceux de Docker ou Podman. Quarkus va même encore plus loin, en permettant de transformer l’application Java en un exécutable natif ! Voici une rapide introduction, par la pratique, à cet incroyable framework, qui nous offrira l’opportunité d’illustrer également sa facilité de prise en main.

De la scytale au bit quantique : l’avenir de la cryptographie

Magazine
Marque
Contenu Premium
Spécialité(s)
Résumé

Imaginez un monde où nos données seraient aussi insaisissables que le célèbre chat de Schrödinger : à la fois sécurisées et non sécurisées jusqu'à ce qu'un cryptographe quantique décide d’y jeter un œil. Cet article nous emmène dans les méandres de la cryptographie quantique, où la physique quantique n'est pas seulement une affaire de laboratoires, mais la clé d'un futur numérique très sécurisé. Entre principes quantiques mystérieux, défis techniques, et applications pratiques, nous allons découvrir comment cette technologie s'apprête à encoder nos données dans une dimension où même les meilleurs cryptographes n’y pourraient rien faire.

Les listes de lecture

9 article(s) - ajoutée le 01/07/2020
Vous désirez apprendre le langage Python, mais ne savez pas trop par où commencer ? Cette liste de lecture vous permettra de faire vos premiers pas en découvrant l'écosystème de Python et en écrivant de petits scripts.
11 article(s) - ajoutée le 01/07/2020
La base de tout programme effectuant une tâche un tant soit peu complexe est un algorithme, une méthode permettant de manipuler des données pour obtenir un résultat attendu. Dans cette liste, vous pourrez découvrir quelques spécimens d'algorithmes.
10 article(s) - ajoutée le 01/07/2020
À quoi bon se targuer de posséder des pétaoctets de données si l'on est incapable d'analyser ces dernières ? Cette liste vous aidera à "faire parler" vos données.
Voir les 64 listes de lecture

Abonnez-vous maintenant

et profitez de tous les contenus en illimité

Je découvre les offres

Déjà abonné ? Connectez-vous