Les enums Python
Apprenez les enums Python : créez Enum, IntEnum, Flag et auto(), ajoutez des méthodes, comparez en toute sécurité et éliminez les nombres magiques de votre code.
Un enum (abréviation d'énumération) est un ensemble de valeurs nommées et constantes regroupées sous un seul type. Au lieu de disperser des entiers ou des chaînes brutes comme 1, 2, "pending", "active" dans votre code, vous donnez à chacun un nom descriptif — Status.PENDING, Color.RED — et Python garantit que ce nom correspond toujours à la même valeur.
Ce chapitre couvre :
- Pourquoi les enums existent et quels problèmes ils résolvent
- Créer un enum avec la classe
Enum - Accéder aux membres par nom et par valeur
- Itérer sur un enum
auto()— laisser Python assigner les valeurs automatiquementIntEnum— des enums qui se comportent comme des entiersFlag— des enums combinables à bits d'indicateur- Ajouter des méthodes et des propriétés à un enum
- Les alias,
@uniqueet_missing_ - Quand utiliser les enums plutôt que d'autres patterns
Avant de lire ce chapitre, assurez-vous d'être à l'aise avec les classes et objets Python et les types de données Python.
Pourquoi utiliser les enums ?
Considérons cette fonction qui traite un statut de commande passé sous forme d'entier brut :
def handle_order(status):
if status == 1:
print("Order is pending")
elif status == 2:
print("Order is active")
elif status == 3:
print("Order is complete")Cela fonctionne, mais pose de vrais problèmes :
- Nombres magiques. Que signifie
2isolément ? Il faut remonter à la définition de la fonction. - Aucune validation.
handle_order(99)ne fait rien silencieusement — aucune erreur, aucun avertissement. - Les fautes de frappe sont invisibles.
handle_order(2)ethandle_order(20)sont tous les deux du Python valide. - La refactorisation est risquée. Si vous décidez que
1doit signifier autre chose, vous devez retrouver chaque1dans la base de code.
Les enums règlent tous ces problèmes. La même logique écrite avec un enum est auto-documentée, sûre et facile à refactoriser :
from enum import Enum
class OrderStatus(Enum):
PENDING = 1
ACTIVE = 2
COMPLETE = 3
def handle_order(status: OrderStatus):
if status == OrderStatus.PENDING:
print("Order is pending")
elif status == OrderStatus.ACTIVE:
print("Order is active")
elif status == OrderStatus.COMPLETE:
print("Order is complete")
handle_order(OrderStatus.ACTIVE) # Order is activeL'intention est claire, et Python empêche handle_order(99) de correspondre accidentellement à une branche.
Créer un enum
Importez Enum depuis le module enum (faisant partie de la bibliothèque standard Python — aucune installation nécessaire) et créez une sous-classe :
from enum import Enum
class Color(Enum):
RED = 1
GREEN = 2
BLUE = 3Chaque attribut de classe (RED, GREEN, BLUE) devient un membre de l'enum. Les valeurs à droite (1, 2, 3) peuvent être des entiers, des chaînes ou tout autre type — le choix vous appartient.
Accéder aux membres
Il existe trois façons d'accéder à un membre d'un enum :
from enum import Enum
class Color(Enum):
RED = 1
GREEN = 2
BLUE = 3
# Attribute access (most common)
print(Color.RED) # Color.RED
# By name (square bracket notation)
print(Color['GREEN']) # Color.GREEN
# By value (call the class with the value)
print(Color(3)) # Color.BLUEChaque membre expose deux attributs :
print(Color.RED.name) # RED
print(Color.RED.value) # 1Utilisez .name lorsque vous avez besoin d'un libellé lisible par l'humain (pour la journalisation ou l'affichage), et .value lorsque vous devez transmettre la valeur sous-jacente à un système externe (une base de données, une API).
repr et type
print(repr(Color.RED)) # <Color.RED: 1>
print(type(Color.RED)) # <enum 'Color'>Un membre d'enum est une instance de sa classe enum, pas de int ni de str.
Itérer sur un enum
Les enums sont itérables. L'itération produit les membres dans l'ordre de définition :
from enum import Enum
class Color(Enum):
RED = 1
GREEN = 2
BLUE = 3
for color in Color:
print(color.name, color.value)
# RED 1
# GREEN 2
# BLUE 3Vous pouvez également vérifier l'appartenance :
print(Color.RED in Color) # TrueCela rend les enums pratiques pour remplir des listes déroulantes, construire des tables de dispatch de type switch, ou créer des listes de choix pour la saisie utilisateur.
auto() — Valeurs automatiques
Si les valeurs spécifiques n'ont pas d'importance — vous souhaitez seulement que chaque membre soit distinct — utilisez auto(). Python assigne des entiers séquentiels à partir de 1 :
from enum import Enum, auto
class Direction(Enum):
NORTH = auto()
SOUTH = auto()
EAST = auto()
WEST = auto()
for d in Direction:
print(d.name, d.value)
# NORTH 1
# SOUTH 2
# EAST 3
# WEST 4auto() est particulièrement utile lorsque l'enum sera amené à grandir au fil du temps et que vous ne voulez pas renuméroter manuellement les membres.
Comparer les membres d'un enum
Utilisez is ou == pour comparer les membres. Les deux fonctionnent, mais is est légèrement plus rapide car les membres d'enum sont des singletons — chaque nom correspond exactement à un objet :
from enum import Enum
class Color(Enum):
RED = 1
GREEN = 2
BLUE = 3
print(Color.RED is Color.RED) # True
print(Color.RED == Color.RED) # True
print(Color.RED == Color.GREEN) # FalseUn membre Enum ordinaire n'est pas égal à sa valeur brute :
print(Color.RED == 1) # FalseC'est intentionnel. Cela empêche l'égalité accidentelle entre différents enums partageant le même entier :
class Size(Enum):
SMALL = 1
print(Color.RED == Size.SMALL) # False — different typesSi vous avez besoin d'une comparaison basée sur la valeur (par exemple, member > 1), utilisez plutôt IntEnum (voir ci-dessous).
IntEnum — Enums qui se comportent comme des entiers
Les membres IntEnum sont également des entiers Python ordinaires. Vous pouvez donc utiliser l'arithmétique, les opérateurs de comparaison et les passer partout où un int est attendu :
from enum import IntEnum
class Priority(IntEnum):
LOW = 1
MEDIUM = 2
HIGH = 3
print(Priority.HIGH > Priority.LOW) # True
print(Priority.MEDIUM + 10) # 12
print(Priority.HIGH == 3) # TrueUn cas d'utilisation courant est le tri d'une liste de membres d'enum :
from enum import IntEnum
class Level(IntEnum):
LOW = 1
MED = 2
HIGH = 3
levels = [Level.HIGH, Level.LOW, Level.MED]
print([l.name for l in sorted(levels)]) # ['LOW', 'MED', 'HIGH']Quand préférer Enum à IntEnum
La transparence entière d'IntEnum est aussi sa faiblesse : Priority.HIGH == 3 est True, donc un littéral mal tapé 3 sera silencieusement égal à Priority.HIGH. Utilisez Enum ordinaire lorsque vous voulez une sûreté de type stricte, et utilisez IntEnum uniquement lorsque vous avez vraiment besoin de l'arithmétique entière ou devez interagir avec une API qui manipule des nombres bruts.
Flag — Enums à bits d'indicateur combinables
Flag est conçu pour les scénarios où plusieurs options peuvent être actives en même temps. Ses membres sont des puissances de deux, et vous les combinez avec l'opérateur | (OU binaire) :
from enum import Flag, auto
class Permission(Flag):
READ = auto()
WRITE = auto()
EXECUTE = auto()
ALL = READ | WRITE | EXECUTE
user = Permission.READ | Permission.WRITE
print(user) # Permission.WRITE|READ
print(Permission.READ in user) # True
print(Permission.EXECUTE in user) # Falseauto() à l'intérieur de Flag assigne des puissances de deux successives (1, 2, 4, 8, …) afin que la combinaison de membres avec | ne produise jamais de résultats ambigus.
Utilisez Flag pour les systèmes de permissions, les bascules de fonctionnalités, et toute situation où vous avez besoin d'un ensemble compact de commutateurs booléens.
Ajouter des méthodes et des propriétés
Comme un enum est une classe, vous pouvez lui ajouter des méthodes et des propriétés. Cela garde la logique associée à l'intérieur du type plutôt que dispersée dans des chaînes if/elif :
from enum import Enum
class HttpStatus(Enum):
OK = 200
CREATED = 201
NOT_FOUND = 404
INTERNAL_ERROR = 500
@property
def is_success(self):
return 200 <= self.value < 300
@property
def is_error(self):
return self.value >= 400
def handle_response(status: HttpStatus):
if status.is_success:
print(f"{status.value} {status.name}: request succeeded")
elif status.is_error:
print(f"{status.value} {status.name}: request failed")
handle_response(HttpStatus.OK) # 200 OK: request succeeded
handle_response(HttpStatus.NOT_FOUND) # 404 NOT_FOUND: request failedVous pouvez également donner à un enum un __init__ personnalisé pour stocker des données supplémentaires par membre. Fournissez les valeurs sous forme de tuples :
from enum import Enum
class Planet(Enum):
MERCURY = (3.303e+23, 2.4397e6)
VENUS = (4.869e+24, 6.0518e6)
EARTH = (5.976e+24, 6.37814e6)
def __init__(self, mass, radius):
self.mass = mass
self.radius = radius
@property
def surface_gravity(self):
G = 6.67430e-11
return G * self.mass / (self.radius ** 2)
print(round(Planet.EARTH.surface_gravity, 2)) # 9.8
print(round(Planet.MERCURY.surface_gravity, 2)) # 3.7Le tuple (mass, radius) devient les arguments du constructeur ; self.value contient toujours le tuple complet.
Alias et @unique
Si deux membres partagent la même valeur, le second devient un alias — il se résout vers le premier membre. Les alias ne sont pas produits lors de l'itération :
from enum import Enum
class Status(Enum):
ACTIVE = 1
RUNNING = 1 # alias for ACTIVE
print(Status.ACTIVE is Status.RUNNING) # True
print(list(Status)) # [<Status.ACTIVE: 1>]Les alias sont parfois utiles (par exemple, un ancien nom pointant vers un nouveau), mais ils peuvent aussi masquer des fautes de frappe. Appliquez le décorateur @unique pour interdire complètement les valeurs dupliquées :
from enum import Enum, unique
@unique
class Status(Enum):
PENDING = 1
ACTIVE = 2
INACTIVE = 3
# Trying to add a duplicate value to a @unique enum raises ValueError:
# ValueError: duplicate values found in <enum 'Bad'>: B -> A@unique est un bon choix par défaut pour tout enum où un alias accidentel constituerait un bug.
Recherche personnalisée avec _missing_
Par défaut, appeler Color('unknown') lève une ValueError. Vous pouvez surcharger la méthode de classe _missing_ pour gérer les valeurs non reconnues — par exemple, pour effectuer une recherche insensible à la casse :
from enum import Enum
class Color(Enum):
RED = 'red'
GREEN = 'green'
BLUE = 'blue'
@classmethod
def _missing_(cls, value):
if isinstance(value, str):
for member in cls:
if member.value == value.lower():
return member
return None
print(Color('RED')) # Color.RED
print(Color('Green')) # Color.GREEN_missing_ reçoit la valeur qui n'a pas été trouvée. Retournez le membre correspondant, ou None (ce qui laisse Python lever sa ValueError par défaut).
Quand utiliser les enums
Les enums sont le bon choix lorsque :
- Une variable ne peut contenir qu'un ensemble fixe d'états nommés (statut de commande, verbe HTTP, couleur de carte).
- Vous voulez empêcher les valeurs invalides de passer silencieusement.
- Le même concept est comparé en plusieurs endroits et vous souhaitez une source de vérité unique.
- Vous avez besoin d'itérer sur toutes les valeurs valides (remplir un formulaire, documenter une API).
Vous n'avez probablement pas besoin d'un enum lorsque :
- L'ensemble de valeurs est ouvert ou change au moment de l'exécution (utilisez un dictionnaire ou une table de correspondance en base de données).
- Vous n'avez besoin que de deux états —
True/Falseavec une signification booléenne claire est plus simple. - Les valeurs proviennent d'une saisie utilisateur qui doit être validée par rapport à un schéma — envisagez une bibliothèque comme Pydantic, qui s'intègre parfaitement avec les enums Python.
Pour des patterns étroitement liés, consultez les dataclasses Python (pour les données structurées avec des valeurs par défaut) et les classes abstraites Python (pour imposer des contrats d'interface entre les sous-classes). Si vous avez besoin de conteneurs de constantes nommées sans toute la machinerie des enums, le module collections Python propose namedtuple comme alternative.