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

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 nouvelles menaces liées à l’intelligence artificielle

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

Sommes-nous proches de la singularité technologique ? Peu probable. Même si l’intelligence artificielle a fait un bond ces dernières années (elle est étudiée depuis des dizaines d’années), nous sommes loin d’en perdre le contrôle. Et pourtant, une partie de l’utilisation de l’intelligence artificielle échappe aux analystes. Eh oui ! Comme tout système, elle est utilisée par des acteurs malveillants essayant d’en tirer profit pécuniairement. Cet article met en exergue quelques-unes des applications de l’intelligence artificielle par des acteurs malveillants et décrit succinctement comment parer à leurs attaques.

Migration d’une collection Ansible à l’aide de fqcn_migration

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

Distribuer du contenu Ansible réutilisable (rôle, playbooks) par l’intermédiaire d’une collection est devenu le standard dans l’écosystème de l’outil d’automatisation. Pour éviter tout conflit de noms, ces collections sont caractérisées par un nom unique, formé d’une espace de nom, qui peut-être employé par plusieurs collections (tel qu'ansible ou community) et d’un nom plus spécifique à la fonction de la collection en elle-même. Cependant, il arrive parfois qu’il faille migrer une collection d’un espace de noms à un autre, par exemple une collection personnelle ou communautaire qui passe à un espace de noms plus connus ou certifiés. De même, le nom même de la collection peut être amené à changer, si elle dépasse son périmètre d’origine ou que le produit qu’elle concerne est lui-même renommé.

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