Les enums en Python


Je n’ai jamais eu besoin d’un type exactement comme les d’Enum en Python de ma vie. Jamais eu le cas d’utilisation. Alors quand la feature est arrivé en python 3.4, cela a été un non-évènement pour moi.

Pourquoi faire ?

Je comprends parfaitement que des langages comme le C ou le Java en aient besoin. Leur syntaxe amène à ce genre de chose. Leur typage aussi. Mais Python ? Il y a tellement de manières de représenter les données, de les manipuler, de les passer.

Les enums c’étaient vraiment pour la déco à mes yeux.

Mais la fonctionnalité a fait débat. Si, si, on peut devenir tout rouge à propos d’une séquence de constantes.

Regardons un peu plus près de quoi il est question.

L’ancien statuquo

D’abord, en Python, on définit généralement les constantes ainsi :

FOO = 1
BAR = 2 
DOH = 3
OYO = 4

Ce ne sont pas vraiment des constantes. Il n’y a rien qui ne puisse être réassigné en Python, même si parfois ça demande un peu de bidouillage, mais c’est une convention que les gens respectent.

Comme les modules sont des singleton, on peut facilement faire:

import module print(module.FOO)

Parfois on a besoin d’un peu plus. On veut plusieurs groupes de constantes. Alors on peut utiliser une classe.

class Stuff:     
    FOO = 1     
    BAR = 2     
    DOH = 3
    OYO = 4

Et on peut faire:

from module import Stuff 
print(Stuff.FOO)

Après, il y a toujours les variantes des gens qui veulent itérer dessus.

from module import Stuff 
for attr, value in Stuff.__dict__.items():     
    print(attr, value)

Et puis il y a les gens qui veulent itérer dessus de manière ordonnée. On leur conseille généralement d’utiliser namedtuple:

from collections import namedtuple
Stuff = namedtuple('Stuff', 'FOO BAR DOH OYO')
stuff = Stuff(*range(1, 5)) 
for val in stuff:     
    print(val)

Mais certains vont vouloir les noms avec les valeurs. Ils se tourneront peut être vers OrderedDict:

from collections import OrderedDict
Stuff = OrderedDict((('FOO', 1), ('BAR', 2), ('DOH', 3), ('OYO', 4))) 
for attr, value in Stuff.items():     
    print(attr, value)

Mais pas d’accès direct par attribut, et il faut faire gaffe à la saisie ou quand on change les valeurs, etc.

Bref, les quelques personnes qui voulaient les enums n’étaient parfaitement satisfaites de ces solutions.

Ce que font les enums en Python

En gros, les Enums sont des classes bridées qui représentent des itérables de constantes. Chaque constante est un couple clé/valeur.

from enum import Enum
 
class Stuff(Enum):     
    FOO = 1     
    BAR = 2     
    DOH = 3     
    OYO = 4
 
for member in Stuff:      
    print(member.name, member.value)

Les enums peuvent être héritées (mais de manière limitée) et implémenter un peu de logique puisque ce sont finalement des classes.

Il existe un type IntEnum pour que chaque membre soit comparable à un int directement plutôt que de voir chercher .value, qui par ailleurs peut être n’importe quel type avec Enum.

Et on a une syntaxe raccourcie:

Stuff = Enum('Stuff', 'FOO BAR DOH OYO')

Bref, la feature ne fait pas grand-chose, en tout cas rien de ce qu’on ne pouvait faire avant. Mais elle a quelques avantages :

  • Les enums sont une forme de documentation par le code. Quand on en lit une, on sait que ça va être un groupe de valeurs de configuration.
  • Ca s’inspecte bien et l’affichage est clair.
  • C’est picklable.
  • Ca permet de faire des wrappers pour les langages qui eux utilisent beaucoup les enums ou des structs. Comme Python sert souvent de glue, c’est logique de vouloir un type qui matche.
  • On peut choisir entre des membres comparables entre enums (IntEnum) et avec les int, ou pas (Enum).
  • Les membres peuvent avoir des alias.

Mais tout ce temps après avoir vu les enums introduites, je n’en ai toujours pas l’usage. Jamais assez de constantes, ou alors quand j’en ai, je peux en faire des tuples ou des dicos sans avoir besoin d’un import.

Je suppose que je fais pas assez d’extensions C.

Quoiqu’il en soit généralement quand je vois des gens demander des enums, ce sont souvent pour de mauvaises raisons, pour reproduire ce qu’ils ont l’habitude d’utiliser dans un autre langage. Mais il y a vraiment peu de use cases typiquement Python pour les enums.

Allez, si vous avez quelques flags à mettre quelque part ou une API à wrapper, mettre une enum ne fera pas de mal.

17 thoughts on “Les enums en Python

  • entwanne

    Il semble y avoir quelques problèmes de formatage sur les codes d’exemples présentés, plusieurs sauts de ligne n’apparaissent pas.

  • drezr

    Premier commentaire sur le blog que je lis depuis quelques mois, j’en profite pour vous remercier pour toutes les connaissances partagées et signaler : “a fait débat” dead link.

    L’image de l’article est insane :>

  • inso

    Quoiqu’il en soit généralement quand je vois des gens demander des enums, ce sont souvent pour de mauvaises raisons, pour reproduire ce qu’ils ont l’habitude d’utiliser dans un autre langage. Mais il y a vraiment peu de use cases typiquement Python pour les enums.

    As-tu des exemples de mauvaise utilisation ? Dans quelle situation les enums te semblent adaptées, et dans quelles situation classique aux autres langages elles seraient mal utilisées ?

    Par exemple, pour une machine à état, utiliser des enums pour représenter l’état de l’objet, bien ou pas bien ?

  • RJ45

    Je tenais simplement à dire que l’image de l’article est vraiment dégueux. J’adore, bravo

  • Sam Post author

    @inso: pour une machine a état, on peut stocker l’état lui-même (qui est un objet), pas besoin d’un intermédiaire. Si on veut quelque chose qui représente cet état sans l’etre (par exemple pour un fichier de conf), autant utiliser une simple string, c’est simple à maninpuler, namespacable et sans import.

    Une string pour un mode est largement suffisant.

    Un usage idiot:

    http.request(TYPE.GET, url)

    Alors que:

    http.requests(‘GET’, url)

    Ou encore:

    http.get(url)

    Font très bien l’affaire.

  • DrHaze

    J’ai du me mord l’intérieur des joues tellement fort pour pas éclater de rire dans l’open-space quand j’ai vu la vignette de l’article…

  • LeBouquetin

    @Sam, je ne vois pas en quoi l’usage que tu présentes (TYPE.GET) est idiot. J’aurais même tendance à dire que c’est idiot de trouver ça idiot :

    l’exemple que tu prends se base sur les verbes HTTP qui sont d’une part peu nombreux, et d’autre part normalisés. C’est rarement le cas du code qu’on écrit. Par exemple si tu veux gérer le statut d’un type d’objets, le fait d’utiliser un enum’ permet de normaliser la liste des valeurs possibles, donc il est plus rapide de monter en compétence sur le code
    l’utilisation de “string” est un anti-pattern, et pose de gros problèmes en terme de refactoring, mais également d’efficacité : le mec qui tape ‘GEt’ va perdre du temps à détecter qu’il a fait une faute de frappe là où l’utilisation d’une constante permettra de visualiser l’erreur immédiatement (par exemple avec PyCharm, c’est le cas)

    Vouloir utiliser des string “parce que c’est plus simple/souple” c’est comme de dire “on va utiliser des dictionnaires au lieu de classes”. Tant que tu travailles tout seul, c’est pas un problème, par contre quand tu travailles en équipe, c’est bien de savoir ce que le code de ton collègue attend comme structure, c’est de l’auto-documentation.

  • Sam Post author

    Mais justement, il vaut mieux utiliser des dictionnaires que des classes dans bien des cas. Utiliser des classes ou des enums à tout vas en Python est le signe qu’on a pas laché les habitudes d’autres langages.

    Par exemple, tu parles de refactoring. Mais en en Python le refactoring est rarement un gros problème comme dans d’autres langage. Le duck typing, le typage dynamique et les outils mis à disposition font que c’est presque toujours beaucoup plus facile et rapide en Python.

    Il faut utiliser les outils dans un contexte où leurs bénéfices sont supérieurs à leur coût. En Python, de nombreux outils d’autres langages sont inutilement lourds ou compliqués. Chaque usage d’un outil de plus demande à connaitre une chose de plus pour comprendre et utiliser le code.

  • ast

    Dans la ligne

    Stuff = namedtuple(‘Stuff’, ‘FOO BAR DOH OYO’)(*range(1, 5))

    Le 1er “Stuff” est un objet, pas une classe, le nom devrait commencer par une minuscule, stuff.

    C’est namedtuple(‘Stuff’, ‘FOO BAR DOH OYO’) qui est une classe de nom Stuff

    ça m’a perturbé un moment

  • rewk

    Un des usages que j’ai des enums est de fournir une liste de choices à argparse. Mettons que je veuille faire un setteur qui prend des valeurs discrètes.

    parser.add_argument("--set", choices=[monenum.value for monenum in MonEnum])

    Comme ça, je peux facilement lier un affichage avec un identifieur qui ne sera pas forcément le même.

    Je sais que la valeur est correcte et validée par argparse. Je peux alors facilement utiliser MonEnum(args.set) dans le reste du code.

    Je trouve ça sympa de ne pas avoir à risquer de me tromper quand la valeur de l’enum n’est pas la même que le nom.

  • Youness

    est ce une coicidence que le nom du site est sam et max ou bien c’est une allusion aux fréres sam et max kellerman?

  • Sam Post author

    C’est un allusion au jeu vidéo éponyme unanimement adoré par les co-auteurs.

  • Nicolas

    Je viens de voir un cas d’utilisation dans un tuto qui me semble intéressant. Le contexte : modèle de base de données avec sqlalchemy :

    D’abord on crée un enum avec les valeurs possibles en entrée :

    class Gender(enum.Enum):

    female = 0

    male = 1

    other = 2

    Puis on l’appelle dans la classe modèle

    class Content(db.Model):

    gender = db.Column(db.Enum(Gender), nullable=False)

    Ça permet de faire ensuite le mapping en écriture et en lecture sur ses requêtes. Est-ce que ça aurait marché pareil avec un dico? I don’t know :]

Comments are closed.

Des questions Python sans rapport avec l'article ? Posez-les sur IndexError.