
En Python, la plupart des design patterns du Gang of Four se réduisent à une fonction, un dictionnaire ou un module. Les fonctions de première classe, le duck typing et les modules importables une seule fois couvrent nativement ce que Java résout avec des hiérarchies de classes. Ce guide reprend six patrons classiques : Singleton, Factory, Strategy et Observer, plus Adapter et Decorator, et montre leur forme réellement pythonique. L'objectif n'est pas de réciter le catalogue GoF, mais de savoir quand un patron devient inutile parce que le langage fait déjà le travail. Les exemples ont été exécutés avec Python 3.12.
Ce guide s'adresse aux développeuses et développeurs qui connaissent déjà la programmation orientée objet et veulent écrire du code idiomatique. Avant d'attaquer, ces briques du langage sont mobilisées en permanence.
Ce que vous allez apprendre
Section intitulée « Ce que vous allez apprendre »- Implémenter un Singleton avec un module ou
__new__, et connaître ses réserves. - Remplacer une fabrique par une fonction ou un dictionnaire de constructeurs.
- Écrire une Strategy en passant une fonction plutôt qu'une hiérarchie de classes.
- Construire un Observer avec une liste d'abonnés et des callbacks.
- Reconnaître que Adapter et Decorator sont déjà couverts par
functools. - Éviter de plaquer les patterns Java sur un langage qui n'en a pas besoin.
Pourquoi Python change la donne
Section intitulée « Pourquoi Python change la donne »Le catalogue du Gang of Four (le livre Design Patterns de 1994) a été pensé pour des langages à typage statique et sans fonctions de première classe, comme le C++ et le Java de l'époque. Beaucoup de patrons y compensent une rigidité du langage : pour passer un comportement en paramètre, il fallait créer une interface, puis une classe concrète qui l'implémente, puis instancier cette classe. Trois artefacts pour transporter un simple bout de logique.
Python n'a pas cette contrainte. Une fonction est un objet que l'on stocke dans une variable, place dans une liste ou passe en argument. Le duck typing accepte n'importe quel objet qui expose les bonnes méthodes, sans héritage déclaré. Un module n'est chargé qu'une fois et se comporte comme un objet global partagé. Résultat : certains patrons deviennent triviaux, d'autres carrément superflus. Peter Norvig l'a résumé dès 1996 : 16 des 23 patrons GoF sont invisibles ou simplifiés dans un langage dynamique.
Le Singleton : un module suffit presque toujours
Section intitulée « Le Singleton : un module suffit presque toujours »Le Singleton garantit qu'une classe n'a qu'une seule instance, partagée par tout le programme. C'est utile pour une configuration globale, un pool de connexions ou un cache. En Java, on le construit avec un constructeur privé et une méthode statique. En Python, le besoin réel - un état global unique - est déjà satisfait par un module, puisque l'interpréteur ne l'importe qu'une fois et met le résultat en cache dans sys.modules.
Voici un module reglages.py qui joue le rôle de configuration partagée. N'importe où dans le programme, import reglages renvoie le même objet en mémoire.
# reglages.py : un module est un singleton naturel en Pythonregion = "eu-west-2"niveau_log = "INFO"
def resumer(): return f"region={region} niveau_log={niveau_log}"On modifie l'état à un endroit, on le relit ailleurs, et les deux références pointent vers le même objet. Aucune classe, aucun verrou, aucune méthode statique.
import reglagesimport reglages as autre_reference
reglages.niveau_log = "DEBUG"print("valeur lue ailleurs :", autre_reference.niveau_log)print("meme objet en memoire :", reglages is autre_reference)print(reglages.resumer())L'exécution confirme que les deux noms désignent le même module partagé :
valeur lue ailleurs : DEBUGmeme objet en memoire : Trueregion=eu-west-2 niveau_log=DEBUGSi vous tenez absolument à une classe avec unicité d'instance, la surcharge de __new__ fait le travail. La méthode __new__ construit l'objet avant __init__ : on y renvoie toujours la même instance stockée sur la classe.
class ConnexionBase: _instance = None
def __new__(cls): if cls._instance is None: cls._instance = super().__new__(cls) return cls._instance
def __init__(self): # attention : __init__ est rappele a chaque appel self.uri = "postgresql://localhost/prod"
a = ConnexionBase()b = ConnexionBase()print("meme instance :", a is b)print("uri :", a.uri)Le résultat prouve l'unicité, mais révèle aussi le piège documenté en commentaire :
meme instance : Trueuri : postgresql://localhost/prodLa fabrique : un dictionnaire de constructeurs
Section intitulée « La fabrique : un dictionnaire de constructeurs »La Factory centralise la création d'objets pour que le code appelant n'ait pas à connaître la classe concrète à instancier. En Java, cela passe par une classe abstraite Factory et des sous-classes. En Python, une fonction suffit dans les cas simples, et un dictionnaire qui associe une clé à un constructeur remplace élégamment toute la hiérarchie dès qu'il y a plusieurs variantes.
L'exemple choisit un algorithme de compression d'après une chaîne. Les fonctions de la bibliothèque standard (gzip.compress, bz2.compress, lzma.compress) sont des objets : on les range directement dans un dictionnaire, sans wrapper.
import gzipimport bz2import lzma
# Un dictionnaire de constructeurs remplace une hierarchie de fabriquesCOMPRESSEURS = { "gz": gzip.compress, "bz2": bz2.compress, "xz": lzma.compress,}
def compresser(donnees: bytes, algo: str) -> bytes: try: fonction = COMPRESSEURS[algo] except KeyError: raise ValueError(f"algorithme inconnu : {algo}") return fonction(donnees)Le point clé : ajouter un algorithme, c'est ajouter une ligne au dictionnaire, sans toucher à compresser. C'est le principe ouvert/fermé obtenu sans une seule classe. La gestion d'une clé absente lève une erreur explicite au lieu d'un KeyError brut.
texte = b"journal applicatif " * 50for algo in ("gz", "bz2", "xz"): resultat = compresser(texte, algo) print(f"{algo:>3} : {len(texte)} octets -> {len(resultat)} octets")
try: compresser(texte, "rar")except ValueError as erreur: print("erreur attendue :", erreur)Chaque algorithme comprime réellement les données, et la clé inconnue déclenche le message dédié :
gz : 950 octets -> 48 octetsbz2 : 950 octets -> 75 octets xz : 950 octets -> 96 octetserreur attendue : algorithme inconnu : rarLa Strategy : passez une fonction, pas une hiérarchie
Section intitulée « La Strategy : passez une fonction, pas une hiérarchie »Le patron Strategy rend un algorithme interchangeable à l'exécution. Le cas d'école GoF crée une interface Strategy et une classe par variante, puis injecte l'objet stratégie dans le contexte. En Python, comme une fonction est un objet transportable, on passe directement la fonction en argument. Toute la mécanique de classes disparaît.
Cet exemple calcule le prix d'une commande selon une règle de remise variable. Chaque stratégie est une fonction qui reçoit la commande et renvoie un montant de remise.
from dataclasses import dataclassfrom collections.abc import Callable
@dataclassclass Commande: montant: float lignes: int
# Chaque strategie est une simple fonction, pas une classedef remise_zero(c: Commande) -> float: return 0.0
def remise_fidelite(c: Commande) -> float: return c.montant * 0.05
def remise_volume(c: Commande) -> float: return 10.0 if c.lignes >= 10 else 0.0
def prix_final(c: Commande, remise: Callable[[Commande], float]) -> float: return c.montant - remise(c)Le type Callable[[Commande], float] documente le contrat de la stratégie : une fonction qui prend une commande et rend un flottant. La fonction prix_final ne connaît aucune classe de remise, elle appelle juste l'objet reçu. On peut même fournir une lambda créée sur place pour une promotion ponctuelle.
commande = Commande(montant=200.0, lignes=12)print("sans remise :", prix_final(commande, remise_zero))print("fidelite :", prix_final(commande, remise_fidelite))print("volume :", prix_final(commande, remise_volume))
# On peut aussi passer une lambda a la voleeprint("promo lambda :", prix_final(commande, lambda c: c.montant * 0.20))Chaque règle produit un prix distinct sans qu'on ait écrit la moindre sous-classe :
sans remise : 200.0fidelite : 190.0volume : 190.0promo lambda : 160.0C'est l'un des cas où le patron GoF s'évapore presque entièrement. La « hiérarchie de stratégies » se résume à un paramètre de type fonction. Ajouter une politique de remise, c'est écrire une nouvelle fonction, jamais une classe et son interface.
L'Observer : une liste d'abonnés et des callbacks
Section intitulée « L'Observer : une liste d'abonnés et des callbacks »Le patron Observer notifie plusieurs objets quand un sujet change d'état : c'est le cœur des systèmes événementiels. La version GoF impose une interface Observer avec une méthode update, et chaque abonné est une classe. En Python, un abonné est simplement une fonction (un callback), et le sujet garde une liste de fonctions à rappeler. Le duck typing rend l'interface implicite : tout objet appelable convient.
Ici, un capteur de température notifie ses abonnés à chaque mesure. Les abonnés sont deux fonctions aux rôles distincts : journaliser et alerter.
from collections.abc import Callable
class Capteur: def __init__(self) -> None: self._abonnes: list[Callable[[float], None]] = []
def abonner(self, rappel: Callable[[float], None]) -> None: self._abonnes.append(rappel)
def mesurer(self, temperature: float) -> None: for rappel in self._abonnes: rappel(temperature)
def journaliser(temp: float) -> None: print(f"[journal] temperature = {temp} C")
def alerter(temp: float) -> None: if temp > 30: print(f"[alerte] seuil depasse : {temp} C")La méthode abonner empile un callback dans la liste, et mesurer les rappelle tous dans l'ordre. Aucun abonné n'hérite d'une classe de base : ce sont des fonctions ordinaires, ce qui les rend triviales à tester isolément.
capteur = Capteur()capteur.abonner(journaliser)capteur.abonner(alerter)
capteur.mesurer(22.5)capteur.mesurer(34.0)Les deux abonnés réagissent, mais alerter ne parle qu'au-dessus du seuil :
[journal] temperature = 22.5 C[journal] temperature = 34.0 C[alerte] seuil depasse : 34.0 CAdapter et Decorator : déjà natifs dans functools
Section intitulée « Adapter et Decorator : déjà natifs dans functools »Deux patrons GoF sont si bien intégrés au langage qu'on les écrit sans y penser. L'Adapter convertit une interface vers celle attendue par le code appelant. Le Decorator ajoute un comportement autour d'un objet existant sans le modifier. Python fournit une syntaxe dédiée pour le second (@) et le module functools pour les deux.
Un adaptateur se réduit souvent à une fonction de conversion ou à functools.partial, qui fige des arguments d'une fonction existante pour lui donner une nouvelle signature. Voici un adaptateur qui normalise des lignes brutes d'une API tierce vers des flottants exploitables.
from collections.abc import Iterator
# Une API tierce renvoie des lignes brutesdef lire_capteur_externe() -> list[str]: return ["21.4;ok", "29.8;ok", "35.1;alerte"]
# L'adaptateur convertit vers le format attendu par notre codedef temperatures(source) -> Iterator[float]: for ligne in source(): valeur, _statut = ligne.split(";") yield float(valeur)
import functools
def envoyer(canal: str, message: str) -> str: return f"[{canal}] {message}"
envoyer_slack = functools.partial(envoyer, "slack")
for valeur in temperatures(lire_capteur_externe): print(f"temperature normalisee : {valeur}")
print(envoyer_slack("deploiement termine"))La fonction temperatures adapte un format texte;statut vers un flux de nombres, et partial adapte la signature de envoyer en une fonction à un seul argument. Le résultat parle de lui-même :
temperature normalisee : 21.4temperature normalisee : 29.8temperature normalisee : 35.1[slack] deploiement termineLe Decorator, lui, a carrément donné son nom à une syntaxe du langage. Un décorateur enveloppe une fonction pour lui ajouter un comportement transverse - chronométrage, cache, journalisation - sans toucher son code. La bonne pratique impose functools.wraps, qui préserve le nom et la docstring de la fonction d'origine.
import functoolsimport time
def chronometrer(fonction): @functools.wraps(fonction) def enveloppe(*args, **kwargs): debut = time.perf_counter() resultat = fonction(*args, **kwargs) duree = time.perf_counter() - debut print(f"{fonction.__name__} a pris {duree:.4f} s") return resultat return enveloppe
@chronometrerdef somme_carres(n: int) -> int: """Additionne les carres de 0 a n-1.""" return sum(i * i for i in range(n))
print("resultat :", somme_carres(1_000_000))print("nom conserve :", somme_carres.__name__)print("docstring conservee :", somme_carres.__doc__)La sortie montre le comportement ajouté et l'identité préservée grâce à wraps (la durée varie selon la machine) :
somme_carres a pris 0.0347 sresultat : 333332833333500000nom conserve : somme_carresdocstring conservee : Additionne les carres de 0 a n-1.Autrement dit, quand vous écrivez @functools.lru_cache ou @functools.wraps, vous utilisez le patron Decorator industrialisé par la bibliothèque standard. Inutile de réimplémenter le catalogue : le langage l'a absorbé.
Ne plaquez pas les patterns Java sur Python
Section intitulée « Ne plaquez pas les patterns Java sur Python »La leçon transverse de ce guide tient en une phrase : un design pattern est une réponse à une contrainte de langage, pas une vérité universelle. Quand la contrainte disparaît, le patron disparaît avec elle. Vouloir reproduire en Python la structure de classes d'un exemple Java aboutit à du code verbeux, plus lent et paradoxalement moins lisible pour un Pythoniste.
Le tableau suivant résume la traduction pythonique de chaque patron abordé. La colonne de droite est presque toujours plus courte et plus directe.
| Patron GoF | Réflexe Java | Version pythonique |
|---|---|---|
| Singleton | Constructeur privé, méthode statique | Un module importable |
| Factory | Classe abstraite + sous-classes | Dict de constructeurs ou fonction |
| Strategy | Interface + une classe par variante | Fonction passée en argument |
| Observer | Interface Observer par abonné | Liste de callbacks |
| Adapter | Classe d'adaptation dédiée | Fonction ou functools.partial |
| Decorator | Classe wrapper implémentant l'interface | Syntaxe @ + functools.wraps |
Cela ne veut pas dire que les classes sont mauvaises. Elles restent le bon outil quand vous manipulez un état riche, un invariant à protéger ou une famille d'objets aux comportements proches. Le message est de partir du besoin, pas du catalogue : écrivez la solution la plus simple qui marche, et laissez le patron émerger seulement s'il clarifie l'intention.
Contrôle de connaissances
Section intitulée « Contrôle de connaissances »Vérifiez que l'essentiel de ce guide est acquis. Les questions portent uniquement sur ce qui vient d'être expliqué ici.
Contrôle de connaissances
Validez vos connaissances avec ce quiz interactif
Informations
- Le chronomètre démarre au clic sur Démarrer
- Questions à choix multiples, vrai/faux et réponses courtes
- Vous pouvez naviguer entre les questions
- Les résultats détaillés sont affichés à la fin
Lance le quiz et démarre le chronomètre
Vérification
(0/0)Profil de compétences
Quoi faire maintenant
Ressources pour progresser
Des indices pour retenter votre chance ?
Nouveau quiz complet avec des questions aléatoires
Retravailler uniquement les questions ratées
Retour à la liste des certifications
À retenir
Section intitulée « À retenir »- Un design pattern compense une contrainte de langage ; Python en supprime beaucoup avec les fonctions de première classe et les modules.
- Le Singleton se fait le plus proprement avec un module ; la version
__new__rejoue__init__et crée un état global difficile à tester. - La Factory devient un dictionnaire de constructeurs : ajouter une variante, c'est ajouter une ligne, pas une classe.
- La Strategy se réduit à passer une fonction (ou une lambda) en argument, typée
Callable. - L'Observer est une liste de callbacks rappelés en boucle ; c'est le socle des signaux Django et Flask.
- Adapter et Decorator sont couverts par
functools.partial,functools.wrapset la syntaxe@. - Ne plaquez pas une hiérarchie de classes Java sur Python : partez du besoin, laissez le patron émerger.
Pour aller plus loin
Section intitulée « Pour aller plus loin »- itertools et functools : la boîte à outils fonctionnelle qui rend beaucoup de patrons superflus.
- Dataclasses Python : structurer des objets de données sans code répétitif, comme la
Commandede la Strategy. - Type hints en Python : annoter proprement les signatures
Callablede vos stratégies et observateurs. - Modules et packages : comprendre pourquoi un module importé une seule fois fait un Singleton fiable.