23. Design Patterns Pythoniques
Singleton, Factory, Strategy, Observer, Repository, Adapter, Decorator (pattern), context manager pattern, patterns idiomatiques Python.
23.1 Singleton
Garantir une seule instance d’une classe.
Via métaclasse
class SingletonMeta(type):
_instances = {}
def __call__(cls, *args, **kwargs):
if cls not in cls._instances:
cls._instances[cls] = super().__call__(*args, **kwargs)
return cls._instances[cls]
class Config(metaclass=SingletonMeta):
def __init__(self):
self.paramètres = {}Via __new__
class Config:
_instance = None
def __new__(cls, *args, **kwargs):
if cls._instance is None:
cls._instance = super().__new__(cls)
cls._instance._initialisé = False
return cls._instance
def __init__(self):
if not self._initialisé:
self.paramètres = {}
self._initialisé = TrueVia module (le plus pythonique)
# config.py — un module est un singleton par nature
class _Config:
...
config = _Config()Utilisation : from config import config.
23.2 Factory
Créer des objets sans exposer la logique d’instanciation.
Factory simple
class Animal:
def parler(self): ...
class Chien(Animal):
def parler(self): return "Woof"
class Chat(Animal):
def parler(self): return "Miaou"
def créer_animal(type_animal: str) -> Animal:
animaux = {"chien": Chien, "chat": Chat}
cls = animaux.get(type_animal)
if cls is None:
raise ValueError(f"Type inconnu: {type_animal}")
return cls()Factory avec registre (pattern plus flexible)
class AnimalFactory:
_registry: dict[str, type[Animal]] = {}
@classmethod
def register(cls, nom: str):
def décorateur(animal_cls: type[Animal]) -> type[Animal]:
cls._registry[nom] = animal_cls
return animal_cls
return décorateur
@classmethod
def créer(cls, nom: str, **kwargs) -> Animal:
animal_cls = cls._registry.get(nom)
if animal_cls is None:
raise ValueError(f"Type inconnu: {nom}")
return animal_cls(**kwargs)
@AnimalFactory.register("chien")
class Chien(Animal):
def __init__(self, race="berger"):
self.race = race23.3 Strategy
Sélectionner un algorithme à l’exécution.
from typing import Protocol
class StratégieTri(Protocol):
def trier(self, données: list[int]) -> list[int]: ...
class TriBulles:
def trier(self, données: list[int]) -> list[int]:
...
class TriRapide:
def trier(self, données: list[int]) -> list[int]:
...
class ContexteTri:
def __init__(self, stratégie: StratégieTri):
self.stratégie = stratégie
def exécuter(self, données: list[int]) -> list[int]:
return self.stratégie.trier(données)Version fonctionnelle (plus pythonique) :
from typing import Callable
def tri_bulles(données: list[int]) -> list[int]: ...
def tri_rapide(données: list[int]) -> list[int]: ...
class ContexteTri:
def __init__(self, stratégie: Callable[[list[int]], list[int]]):
self.stratégie = stratégie
def exécuter(self, données: list[int]) -> list[int]:
return self.stratégie(données)Cas concret : GAR en ML distribué.
from typing import Protocol
import numpy as np
class Agrégateur(Protocol):
"""Stratégie d'agrégation de gradients byzantins."""
def agréger(self, gradients: np.ndarray) -> np.ndarray: ...
class Médiane:
def agréger(self, gradients: np.ndarray) -> np.ndarray:
return np.median(gradients, axis=0)
class Krum:
def agréger(self, gradients: np.ndarray) -> np.ndarray:
...
class Moyenne:
def agréger(self, gradients: np.ndarray) -> np.ndarray:
return np.mean(gradients, axis=0)
class ApprentissageDistribué:
def __init__(self, agrégateur: Agrégateur):
self.agrégateur = agrégateur23.4 Observer
Notifier des dépendants d’un changement d’état.
from typing import Protocol
class Observateur(Protocol):
def mettre_à_jour(self, sujet: "Sujet") -> None: ...
class Sujet:
def __init__(self):
self._observateurs: list[Observateur] = []
def attacher(self, obs: Observateur) -> None:
self._observateurs.append(obs)
def détacher(self, obs: Observateur) -> None:
self._observateurs.remove(obs)
def notifier(self) -> None:
for obs in self._observateurs:
obs.mettre_à_jour(self)Version avec événements (plus pythonique) :
from dataclasses import dataclass, field
from typing import Callable
@dataclass
class ÉmetteurÉvénements:
_callbacks: dict[str, list[Callable]] = field(default_factory=lambda: {})
def on(self, événement: str, callback: Callable):
self._callbacks.setdefault(événement, []).append(callback)
def émettre(self, événement: str, **données):
for cb in self._callbacks.get(événement, []):
cb(**données)
emetteur = ÉmetteurÉvénements()
emetteur.on("train:epoch_end", lambda epoch, loss: print(f"Epoch {epoch}: {loss}"))
emetteur.émettre("train:epoch_end", epoch=5, loss=0.02)23.5 Repository
Abstraction de la couche de stockage.
from typing import Protocol, Optional
class Repository(Protocol):
def get(self, id: str) -> Optional[dict]: ...
def save(self, entity: dict) -> None: ...
def delete(self, id: str) -> None: ...
def list(self) -> list[dict]: ...
class MémoireRepo:
def __init__(self):
self._data: dict[str, dict] = {}
def get(self, id: str) -> Optional[dict]:
return self._data.get(id)
def save(self, entity: dict) -> None:
self._data[entity["id"]] = entity
def delete(self, id: str) -> None:
self._data.pop(id, None)
def list(self) -> list[dict]:
return list(self._data.values())
class JSONRepo:
def __init__(self, chemin: str):
self.chemin = chemin
def get(self, id: str) -> Optional[dict]: ...
def save(self, entity: dict) -> None: ...
def delete(self, id: str) -> None: ...
def list(self) -> list[dict]: ...23.6 Adapter
Rendre une interface incompatible compatible.
# Interface cible
class APIEntraînement(Protocol):
def fit(self, X, y) -> None: ...
def predict(self, X): ...
# Bibliothèque externe avec interface différente
class SklearnModel:
def fit_transform(self, X, y): ...
def infer(self, X): ...
# Adaptateur
class AdaptateurSKLearn:
def __init__(self, model):
self._model = model
def fit(self, X, y):
self._model.fit_transform(X, y)
def predict(self, X):
return self._model.infer(X)23.7 Decorator (pattern)
Ajouter des responsabilités dynamiquement. Ne pas confondre avec les décorateurs Python (leçon 14).
from typing import Protocol
class Notifieur(Protocol):
def envoyer(self, message: str) -> None: ...
class NotifieurBase:
def envoyer(self, message: str) -> None:
print(f"Notification: {message}")
class NotifieurDecorator:
def __init__(self, wrappé: Notifieur):
self._wrappé = wrappé
class NotifieurEmail(NotifieurDecorator):
def envoyer(self, message: str) -> None:
self._wrappé.envoyer(message)
print(f" + Email envoyé")
class NotifieurSMS(NotifieurDecorator):
def envoyer(self, message: str) -> None:
self._wrappé.envoyer(message)
print(f" + SMS envoyé")
n = NotifieurSMS(NotifieurEmail(NotifieurBase()))
n.envoyer("Entraînement terminé")
# Notification: Entraînement terminé
# + Email envoyé
# + SMS envoyé23.8 Patterns idiomatiques Python
Context manager pour resource management
from contextlib import contextmanager
@contextmanager
def session_db(url):
conn = créer_connexion(url)
try:
yield conn
finally:
conn.fermer()EAFP vs LBYL
# LBYL (Look Before You Leap) — Java-style
if "clé" in d and isinstance(d["clé"], int):
valeur = d["clé"]
else:
valeur = 0
# EAFP (Easier to Ask for Forgiveness than Permission) — Pythonic
try:
valeur = d["clé"]
except (KeyError, TypeError):
valeur = 0Duck typing
def sauvegarder(obj):
# Pas de vérification de type — on appelle la méthode
sérialisé = obj.to_dict() # si ça a to_dict(), ça marche
...Descripteur pour validation réutilisable
class Validé:
def __set_name__(self, owner, name):
self.nom = f"_{name}"
def __get__(self, obj, objtype=None):
return getattr(obj, self.nom)
def __set__(self, obj, valeur):
self.valider(valeur)
setattr(obj, self.nom, valeur)
def valider(self, valeur):
...
class Positif(Validé):
def valider(self, valeur):
if valeur < 0:
raise ValueError(f"Doit être positif, reçu {valeur}")
class Point:
x = Positif()
y = Positif()23.9 Tableau récapitulatif
| Pattern | Problème | Solution Pythonique |
|---|---|---|
| Singleton | Instance unique | Module (import) ou métaclasse |
| Factory | Création conditionnelle | Fonction + dict de classes, __init_subclass__ |
| Strategy | Algorithme interchangeable | Protocol + classes, ou Callable |
| Observer | Notification de changement | Callbacks / événements |
| Repository | Abstraction stockage | Protocol + implémentations concrètes |
| Adapter | Interface incompatible | Classe wrapper |
| Decorator (GoF) | Ajout dynamique de responsabilité | Wrapper avec composition |
| EAFP | Gestion d’erreurs | try / except |