backend / python
Design patterns idiomatiques en Python
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.
| Pattern | Solution "à la Java" | Équivalent pythonique |
|---|---|---|
| Strategy | Interface + classes concrètes | Une simple fonction passée en paramètre |
| Singleton | Classe avec instance statique verrouillée | Un module importé (chargé une seule fois) |
| Decorator | Classe 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 StrategieDeTriInterface → StrategieTriAlphabetique → StrategieTriLongueur pour un simple choix de fonction de tri est une sur-ingénierie typique. En Python, Trieur(strategie_tri_longueur) où 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).
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" necessaireRé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.
@dataclasssimplifie fortement les patterns Builder/Value Object.- Les patterns doivent rester des outils, pas des dogmes : privilégier la simplicité pythonique (PEP 20).
Exercices pratiques
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.