Retour au cours

backend / python

Design patterns idiomatiques en Python

Leçon 261 exercice

Explication

Ce que vous allez apprendre

  • Reconnaître les patterns Factory, Strategy, Observer et Builder dans du code Python réel
  • Comprendre pourquoi les fonctions de première classe remplacent souvent des hiérarchies de classes
  • Savoir qu'un module importé se comporte déjà comme un Singleton, sans pattern dédié
  • Construire un objet complexe étape par étape avec un Builder à interface fluide
  • Éviter le piège du dogmatisme : appliquer un pattern seulement quand il résout un vrai problème

Dans quel contexte ?

Un développeur venu de Java rejoint une équipe Python et propose, en revue de code, d'implémenter un Singleton via une métaclasse complexe pour partager une configuration entre plusieurs modules. Un collègue plus expérimenté lui fait remarquer qu'un simple module Python (config.py avec une variable au niveau module) se comporte déjà comme un Singleton, puisqu'il n'est importé et exécuté qu'une seule fois — la solution la plus pythonique est souvent la plus simple.

Des solutions récurrentes, pas des recettes universelles

Les design patterns sont des solutions nommées à des problèmes de conception qui reviennent régulièrement : comment créer un objet sans exposer sa classe exacte, comment notifier plusieurs parties intéressées d'un événement, comment construire un objet complexe étape par étape. Mais attention à un piège fréquent : appliquer mécaniquement des patterns issus de Java ou C++ sans les adapter revient souvent à ajouter de la complexité inutile en Python.

PatternSolution "à la Java"Équivalent pythonique
StrategyInterface + classes concrètesUne simple fonction passée en paramètre
SingletonClasse avec instance statique verrouilléeUn module importé (chargé une seule fois)
DecoratorClasse wrapper imbriquée@decorateur natif du langage

Pourquoi Python simplifie beaucoup de patterns classiques

Python traite les fonctions comme des objets de première classe, ce qui rend inutile toute une hiérarchie de classes que d'autres langages doivent construire pour le pattern Strategy : une simple fonction passée en paramètre suffit. De même, un module Python importé se comporte déjà naturellement comme un Singleton, puisqu'il n'est chargé qu'une seule fois quelle que soit la façon dont il est importé ensuite.

Le vrai risque : le dogmatisme

Le piège n'est pas de connaître les patterns, c'est de les imposer par principe là où une solution plus simple suffirait. Le Zen de Python (PEP 20) rappelle que simple vaut mieux que complexe : un pattern doit résoudre un vrai problème observé, pas anticiper une flexibilité hypothétique qui ne servira jamais.

Piège fréquent

Créer une hiérarchie StrategieDeTriInterfaceStrategieTriAlphabetiqueStrategieTriLongueur pour un simple choix de fonction de tri est une sur-ingénierie typique. En Python, Trieur(strategie_tri_longueur)strategie_tri_longueur est juste une fonction fait exactement la même chose, avec bien moins de code à maintenir.

Ce que cette leçon prépare

Reconnaître ces structures aide surtout à lire du code existant, notamment dans des frameworks comme FastAPI ou Django qui les utilisent abondamment. Les prochaines leçons, plus proches des mécanismes internes de Python (métaclasses déjà vues, internals CPython à venir), donneront les clés pour comprendre comment ces patterns sont réellement implémentés sous le capot.

Commandes & code

Design patterns idiomatiques en Python

Les patterns classiques, adaptés aux spécificités du langage (pas de traduction Java 1:1).

python
from abc import ABC, abstractmethod
from dataclasses import dataclass
from typing import Protocol
import functools

# --- Factory : Python n'a pas besoin d'une hierarchie complexe, une fonction suffit souvent ---
class Vehicule(ABC):
    @abstractmethod
    def rouler(self) -> str: ...

class Voiture(Vehicule):
    def rouler(self) -> str:
        return "Vroom"

class Velo(Vehicule):
    def rouler(self) -> str:
        return "Cling cling"

def creer_vehicule(type_vehicule: str) -> Vehicule:
    fabriques = {"voiture": Voiture, "velo": Velo}
    if type_vehicule not in fabriques:
        raise ValueError(f"Type inconnu : {type_vehicule}")
    return fabriques[type_vehicule]()

# --- Strategy : les fonctions de premiere classe remplacent souvent des classes ---
def strategie_tri_alphabetique(items):
    return sorted(items)

def strategie_tri_longueur(items):
    return sorted(items, key=len)

class Trieur:
    def __init__(self, strategie):
        self.strategie = strategie          # une fonction, pas une interface complexe

    def trier(self, items):
        return self.strategie(items)

trieur = Trieur(strategie_tri_longueur)
print(trieur.trier(["banane", "kiwi", "abricot"]))

# --- Observer : notifier des abonnes lors d'un evenement ---
class SujetObservable:
    def __init__(self):
        self._observateurs = []

    def s_abonner(self, callback):
        self._observateurs.append(callback)

    def notifier(self, *args, **kwargs):
        for callback in self._observateurs:
            callback(*args, **kwargs)

class GestionnaireCommandes(SujetObservable):
    def passer_commande(self, commande):
        print(f"Commande {commande} enregistree")
        self.notifier(commande)

def envoyer_email(commande):
    print(f"Email envoye pour {commande}")

def journaliser(commande):
    print(f"[LOG] Nouvelle commande : {commande}")

gestionnaire = GestionnaireCommandes()
gestionnaire.s_abonner(envoyer_email)
gestionnaire.s_abonner(journaliser)
gestionnaire.passer_commande("CMD-001")

# --- Builder : construire un objet complexe etape par etape ---
@dataclass
class Requete:
    url: str = ""
    methode: str = "GET"
    headers: dict = None
    corps: str = None

class RequeteBuilder:
    def __init__(self):
        self._requete = Requete(headers={})

    def url(self, url):
        self._requete.url = url
        return self          # chainage fluide (fluent interface)

    def methode(self, methode):
        self._requete.methode = methode
        return self

    def header(self, cle, valeur):
        self._requete.headers[cle] = valeur
        return self

    def corps(self, corps):
        self._requete.corps = corps
        return self

    def build(self):
        return self._requete

requete = (
    RequeteBuilder()
    .url("https://api.example.com")
    .methode("POST")
    .header("Content-Type", "application/json")
    .corps('{"cle": "valeur"}')
    .build()
)

# --- Decorator pattern : Python le fait nativement avec @ (voir lecon dediee) ---

# --- Singleton : le module lui-meme EST un singleton en Python (a preferer souvent) ---
# fichier config_singleton.py :
'''
_config = {}

def get_config():
    return _config
'''
# Importer ce module donne toujours la MEME instance de _config -- pas besoin de pattern complexe

# --- Adapter : uniformiser des interfaces incompatibles ---
class ServiceMeteoLegacy:
    def get_temp_fahrenheit(self, ville):
        return 75.0

class AdaptateurMeteoMetrique:
    def __init__(self, service_legacy):
        self._service = service_legacy

    def get_temperature_celsius(self, ville):
        fahrenheit = self._service.get_temp_fahrenheit(ville)
        return (fahrenheit - 32) * 5 / 9

adaptateur = AdaptateurMeteoMetrique(ServiceMeteoLegacy())
print(f"{adaptateur.get_temperature_celsius('Paris'):.1f} C")

# --- Null Object : eviter les verifications "if x is None" repetees ---
class UtilisateurNull:
    nom = "Invite"
    def a_permission(self, permission):
        return False

def obtenir_utilisateur(id_utilisateur):
    return None if id_utilisateur < 0 else UtilisateurNull()  # simplifie ici

utilisateur = obtenir_utilisateur(-1) or UtilisateurNull()
print(utilisateur.nom)     # pas de verification "is None" necessaire

Résumé

  • En Python, les fonctions de première classe remplacent souvent des hiérarchies de classes (Strategy).
  • Un module importé se comporte déjà comme un Singleton : le pattern classique est rarement nécessaire.
  • @dataclass simplifie fortement les patterns Builder/Value Object.
  • Les patterns doivent rester des outils, pas des dogmes : privilégier la simplicité pythonique (PEP 20).

Exercices pratiques

1 disponible
1

Mission : freiner un développeur trop attaché aux patterns Java

Objectif : Remplacer une hiérarchie de classes Strategy inutilement complexe par des fonctions de première classe, et repérer un faux besoin de Singleton.

Contexte

Un développeur venu de Java propose une pull request avec StrategieDeTriInterface, puis StrategieTriAlphabetique(StrategieDeTriInterface) et StrategieTriLongueur(StrategieDeTriInterface), juste pour permettre de choisir entre deux façons de trier une liste. Un autre développeur propose ensuite un Singleton via métaclasse pour partager une configuration entre modules.

Tu dois simplifier la hiérarchie Strategy en fonctions de première classe, expliquer pourquoi le Singleton via métaclasse est superflu ici, puis construire un objet complexe avec un Builder à interface fluide.

Résoudre l’exercice →