Comment utiliser le registry pattern Python sans if else ?

J’utilise un registre pour remplacer les longues chaînes if/elif/else par une table claire de correspondance. Le code devient plus lisible, plus extensible, et surtout moins fragile. Le vrai gain, c’est d’ajouter une option sans retoucher le dispatcher central.

Pourquoi les if else dérapent ?

Un if n’est pas un problème. C’est même souvent la solution la plus lisible quand on a deux cas simples. Le souci arrive quand cette petite décision devient l’endroit où tout le monde vient ajouter sa règle, son modèle, son exception, sa variante métier.

Prenons une fonction get_model. Au départ, elle choisit juste entre deux modèles. Ça tient en quelques lignes, c’est propre, personne ne se plaint.

class OpenAIModel:
    pass

class MistralModel:
    pass

class ClaudeModel:
    pass

class LocalModel:
    pass

def get_model(provider: str, use_cache: bool = True, region: str = "eu"):
    if provider == "openai":
        return OpenAIModel()

    elif provider == "mistral":
        return MistralModel()

    elif provider == "claude":
        return ClaudeModel()

    elif provider == "local":
        return LocalModel()

    else:
        raise ValueError(f"Provider inconnu: {provider}")

À ce stade, ça reste compréhensible. Puis on ajoute une règle pour OpenAI selon la région. Puis un mode fallback. Puis un modèle premium. Puis une option pour les tests. Et là, la fonction ne “choisit” plus seulement un modèle. Elle connaît les fournisseurs, les règles métier, les exceptions, les paramètres techniques, parfois même des détails de configuration.

J’ai vu ça sur un projet client avec une logique de sélection qui devait juste router vers le bon connecteur API. Trois mois plus tard, le bloc faisait presque 200 lignes. Chaque ajout semblait innocent. Mais plus personne n’osait le modifier sans relancer une batterie de tests complète.

Le problème n’est donc pas le if. Le problème, c’est l’accumulation. Chaque nouvelle branche oblige à modifier du code déjà testé. Ça va contre le principe Open/Closed, qui dit qu’un code devrait être ouvert à l’extension, mais fermé à la modification. En clair, je devrais pouvoir ajouter un nouveau modèle sans toucher au dispatcher existant.

Quand tout passe par une grosse chaîne if/elif/else, la complexité cognitive monte vite. Vous devez lire toutes les branches pour comprendre si votre changement casse quelque chose. Et si une équipe externe veut brancher son propre modèle ? Elle doit modifier votre fonction centrale. C’est rarement une bonne idée.

Problème Effet concret Impact maintenance
Branches qui s’accumulent La fonction devient longue et difficile à scanner Chaque modification prend plus de temps
Code déjà testé modifié Un ajout peut casser un ancien cas Risque de régression plus élevé
Responsabilités mélangées Le dispatcher contient de la logique métier et technique Le code devient fragile et moins réutilisable
Extension externe compliquée Un nouveau modèle impose de modifier la fonction centrale Le système est moins ouvert et moins modulaire

C’est quoi le registry pattern Python ?

Le registry pattern Python, c’est une façon simple de remplacer une grosse suite de if / elif / else par une table de correspondance. En pratique, je vois ça comme un petit annuaire interne. Vous donnez une clé, par exemple ‘random_forest’ ou ‘paypal’, et Python retrouve l’objet associé.

Le plus souvent, ce registre est juste un dictionnaire. La clé est une chaîne de caractères. La valeur peut être une classe, une fonction, ou même une instance déjà créée. C’est bête, mais très efficace.

class LinearModel:
    def train(self):
        return "Training linear model"

class RandomForestModel:
    def train(self):
        return "Training random forest model"

class XGBoostModel:
    def train(self):
        return "Training xgboost model"


MODEL_REGISTRY = {
    "linear": LinearModel,
    "random_forest": RandomForestModel,
    "xgboost": XGBoostModel,
}


def get_model(name):
    try:
        model_class = MODEL_REGISTRY[name]
    except KeyError:
        available = ", ".join(MODEL_REGISTRY.keys())
        raise ValueError(
            f"Unknown model '{name}'. Available models are: {available}"
        )

    return model_class()

Ce qui change vraiment, c’est le rôle du code qui choisit quoi exécuter. Il devient un dispatcher léger. Un dispatcher, c’est juste un morceau de code qui reçoit une demande, ici le nom du modèle, puis l’envoie vers le bon objet. Il ne connaît plus tous les détails métier. Il ne sait pas comment marche un Random Forest. Il sait seulement où le trouver.

Et franchement, c’est souvent suffisant. Chez un client, j’ai déjà vu une fonction avec 200 lignes de conditions pour choisir un export, un modèle ou un connecteur API. Le jour où on a remplacé ça par un registre, le code est devenu beaucoup plus lisible. Pas magique. Juste plus propre.

Le dictionnaire apporte deux avantages très concrets :

  • La recherche par clé est généralement très rapide en Python, parce que le dictionnaire est optimisé pour ça.
  • La liste des options disponibles est visible au même endroit, ce qui aide beaucoup quand le projet grossit.

La limite de cette première version, c’est qu’il faut encore modifier MODEL_REGISTRY à chaque fois qu’on ajoute un modèle. Ce n’est pas dramatique au début. Mais sur un gros projet, ou avec des plugins, on voudra souvent aller plus loin avec un enregistrement automatique.

Comment passer au décorateur ?

Le passage au décorateur rend le registre beaucoup plus agréable à maintenir. Au lieu d’avoir un gros bloc central qui connaît tous les moyens de paiement, chaque handler vient s’enregistrer lui-même dans PAYMENT_HANDLERS. Et ça change un truc important : process_payment n’a plus besoin d’être modifié à chaque fois qu’on ajoute Stripe, PayPal, crypto ou le prochain moyen de paiement inventé par le marketing.

PAYMENT_HANDLERS = {}


def register(payment_type):
    def decorator(func):
        PAYMENT_HANDLERS[payment_type] = func
        return func
    return decorator


@register("credit_card")
def charge_credit_card(amount):
    return f"Chargement de {amount}€ par carte bancaire"


@register("paypal")
def charge_paypal(amount):
    return f"Chargement de {amount}€ via PayPal"


@register("crypto")
def charge_crypto(amount):
    return f"Chargement de {amount}€ en crypto"


def process_payment(payment_type, amount):
    handler = PAYMENT_HANDLERS.get(payment_type)

    if handler is None:
        available_types = ", ".join(PAYMENT_HANDLERS.keys())
        raise ValueError(
            f"Moyen de paiement inconnu: {payment_type}. "
            f"Types disponibles: {available_types}"
        )

    return handler(amount)


print(process_payment("paypal", 49.90))
print(process_payment("crypto", 120))

Le décorateur @register(« paypal ») exécute register avec le type de paiement, puis associe automatiquement la fonction au bon nom dans le dictionnaire. C’est simple, mais très puissant. Le dispatcher, ici process_payment, ne sait pas comment charger une carte, PayPal ou une crypto. Il sait juste récupérer une fonction dans un dictionnaire et l’appeler.

Dans la vraie vie, c’est exactement ce que je veux voir sur un projet qui grandit. Pour ajouter un nouveau moyen de paiement, je crée une nouvelle fonction décorée. Point. Je peux même la mettre dans un autre fichier, par exemple payments/stripe.py, tant que ce fichier est importé au démarrage de l’application.

Le code devient plus ouvert à l’extension et moins couplé. Ouvert à l’extension, ça veut dire que je peux ajouter des comportements sans toucher au cœur du système. Moins couplé, ça veut dire que process_payment ne dépend plus directement de toutes les fonctions de paiement. Il dépend seulement du registre.

Petite mise en garde quand même, parce que je l’ai déjà vue chez un client : si le module qui contient les fonctions décorées n’est jamais importé, les décorateurs ne s’exécutent pas. Donc rien ne s’enregistre. Le registre reste vide, et on perd du temps à chercher pourquoi “ça ne marche pas”.

Quand créer une classe Registry ?

Je garde souvent un simple dictionnaire au début. C’est très bien. Si j’ai trois stratégies, deux modèles, ou quelques fonctions à appeler par nom, je ne vais pas sortir une abstraction trop tôt. Mais dès que le même mécanisme revient ailleurs dans le projet, je préfère créer une petite classe Registry.

L’idée est simple : le registre devient responsable de l’enregistrement, de la récupération, des erreurs et des collisions. Le code métier, lui, reste tranquille.

class Registry:
    def __init__(self):
        self._items = {}

    def register(self, name):
        def decorator(item):
            if name in self._items:
                raise KeyError(f"La clé '{name}' existe déjà dans le registre")

            self._items[name] = item
            return item

        return decorator

    def get(self, name):
        try:
            return self._items[name]
        except KeyError:
            available = ", ".join(self.keys())
            raise KeyError(
                f"Clé inconnue '{name}'. Clés disponibles : {available}"
            )

    def keys(self):
        return list(self._items.keys())


model_registry = Registry()


@model_registry.register("xgboost")
class XGBoostModel:
    def train(self, data):
        return "Training XGBoost"


@model_registry.register("linear")
class LinearModel:
    def train(self, data):
        return "Training Linear Model"


def train_model(name, data):
    model_class = model_registry.get(name)
    model = model_class()
    return model.train(data)

Ce que j’aime ici, c’est que le dispatcher ne sait rien des modèles disponibles. Il demande juste au registre : “Donne-moi celui qui correspond à ce nom”. Et ça suffit.

Dans un projet client, on avait commencé avec un dictionnaire posé dans un fichier. Puis un deuxième dictionnaire ailleurs. Puis des erreurs différentes selon les modules. Un jour, une clé a été écrasée sans bruit. Pas dramatique, mais pénible à débugger. La classe Registry règle exactement ce genre de problème.

  • Le comportement du registre est au même endroit.
  • Les messages d’erreur sont cohérents.
  • Une clé déjà utilisée déclenche une erreur claire.
  • Le code métier reste concentré sur son vrai rôle.

Je vois ça comme une frontière propre. Le registre gère les noms et les objets. Le reste du code utilise juste une API stable : register, get, keys.

Approche Lisibilité Extensibilité Complexité
If/else Correct au début, vite lourd Faible, chaque ajout modifie le dispatcher Basse au départ, haute ensuite
Dictionnaire simple Bonne Bonne pour un petit projet Basse
Décorateur Très bonne, l’enregistrement est proche du code Très bonne Moyenne
Classe Registry Très bonne sur la durée Excellente, surtout si le pattern se répète Moyenne, mais maîtrisée

Quels pièges éviter ?

Le registry pattern est très pratique, mais il peut vite devenir un fourre-tout si on ne met pas quelques règles dès le départ. J’ai déjà vu des registres où on trouvait des modèles, des exports CSV, des règles métier, des connecteurs API, et même des bouts de config. À ce moment-là, ce n’est plus un pattern, c’est une poubelle globale avec un joli nom.

Le registre doit rester centré sur une famille claire d’objets ou d’actions. Par exemple des modèles d’IA, des connecteurs, des handlers de paiement, des stratégies de pricing. Si tout le monde peut venir s’accrocher dedans sans logique commune, vous perdez vite le bénéfice principal : la lisibilité.

Je garde en général quelques règles simples pour éviter les mauvaises surprises :

  • Nommer les clés clairement. Une clé comme « stripe_payment » est plus utile que « stripe » si le registre contient des handlers de paiement.
  • Gérer les clés inconnues avec des erreurs explicites. Un message comme « Handler inconnu: paypal. Options disponibles: stripe, adyen » fait gagner du temps.
  • Éviter les collisions silencieuses. Si une clé existe déjà, je préfère lever une erreur plutôt que remplacer l’ancien composant sans prévenir.
  • Documenter les options disponibles. Même un simple commentaire ou une méthode list_available() aide énormément l’équipe.
  • Importer correctement les modules qui s’auto-enregistrent. En Python, un décorateur d’enregistrement ne s’exécute que si le module est importé. C’est bête, mais c’est une source classique de bugs.
  • Ne pas utiliser un registre quand deux if suffisent. Si la logique ne va pas grandir, un registre ajoute juste une couche mentale inutile.

Mon avis est assez direct là-dessus. J’aime ce pattern quand il y a une famille d’objets ou d’actions qui va grossir avec le temps. Des modèles, des connecteurs, des handlers de paiement, des stratégies. Là, il fait vraiment le job. Je ne l’utiliserais pas pour masquer une logique métier complexe qui mérite plutôt d’être repensée. Si le code est confus, le registry pattern ne le rendra pas propre par magie.

Le vrai bénéfice, c’est de garder un dispatcher stable. Le code qui choisit quoi exécuter ne bouge presque plus. L’évolution part dans les composants eux-mêmes, là où elle doit être.

Et si votre prochain if devenait un registre ?

Le registry pattern Python n’est pas une astuce pour faire joli. C’est une manière simple de reprendre le contrôle quand les if/elif/else commencent à grossir. On part souvent d’un dictionnaire, puis on passe au décorateur quand les composants doivent s’enregistrer eux-mêmes. Si le mécanisme revient souvent, une classe Registry propre évite la répétition et les collisions. Je garde quand même une règle simple : si deux conditions suffisent, je ne complexifie pas. Mais dès que le code doit s’étendre, le registre rend le business plus lisible, plus testable, et plus simple à maintenir.

FAQ

  • Qu’est-ce que le registry pattern en Python ?
    Le registry pattern est une façon d’associer une clé à une fonction, une classe ou un objet. En Python, on le fait souvent avec un dictionnaire. Au lieu d’écrire une longue chaîne if/elif/else, on cherche directement l’élément correspondant dans le registre.
  • Pourquoi remplacer les if else par un registre ?
    Parce qu’un registre rend le code plus lisible et plus facile à étendre. Ajouter une nouvelle option ne force pas forcément à modifier le dispatcher central. On réduit le couplage, on limite les risques de régression et on respecte mieux le principe Open/Closed.
  • Un dictionnaire suffit-il pour créer un registry pattern ?
    Oui, dans beaucoup de cas. Un dictionnaire qui mappe des clés vers des fonctions ou des classes est déjà une forme simple de registry pattern. Le décorateur devient intéressant quand on veut que chaque composant s’enregistre lui-même.
  • À quoi sert le décorateur dans un registre Python ?
    Le décorateur permet d’enregistrer une fonction ou une classe au moment où le module est importé. C’est pratique parce qu’on peut ajouter une nouvelle option avec @register(‘nouvelle_cle’) sans toucher au code du dispatcher.
  • Quand faut-il éviter le registry pattern ?
    Je l’évite quand la logique reste très simple. Si deux conditions suffisent, un if clair est souvent meilleur. Le registre devient utile quand on a une famille d’options qui va grandir : modèles, handlers, connecteurs, stratégies ou traitements spécialisés.

 

 

A propos de l’auteur

Je suis Franck Scandolera, expert et formateur en tracking avancé server-side, Analytics Engineering, automatisation No/Low Code avec n8n, intégration de l’IA en entreprise et SEO/GEO. Avec mon agence webAnalyste et l’organisme Formations Analytics, j’accompagne des équipes chez Logis Hôtel, Yelloh Village, BazarChic, la Fédération Française de Football, Texdecor et d’autres. Mon sujet, c’est de rendre les systèmes data, IA et automatisation plus propres, plus fiables et plus maintenables. Si vous voulez structurer vos projets ou former vos équipes, contactez-moi.

Retour en haut