Définition du Singleton en Python
Le Singleton est un design pattern (patron de conception) qui garantit qu'une class ne possède qu'une seule et unique instance dans l'ensemble de votre programme. Chaque appel ultérieur au constructeur retourne toujours la même instance, plutôt que d'en créer une nouvelle.
Ce pattern fait partie des patrons de création (creational patterns) décrits dans le célèbre ouvrage Design Patterns: Elements of Reusable Object-Oriented Software du Gang of Four. En Python, il existe plusieurs façons élégantes de l'implémenter, que nous allons explorer ensemble dans cet article. Si vous souhaitez approfondir vos connaissances en programmation orientée objet, n'hésitez pas à suivre notre formation complète sur Python.
Le Singleton est l'un des design patterns les plus connus, mais aussi l'un des plus débattus. Il est essentiel de comprendre quand l'utiliser — et quand l'éviter.
Pourquoi utiliser un Singleton ?
Le Singleton répond à un besoin précis : contrôler l'accès à une ressource partagée unique. Voici les cas d'usage les plus courants :
- Connexion à une base de données : vous ne souhaitez pas ouvrir plusieurs connexions inutiles.
- Fichier de configuration : un seul objet centralise la lecture de la configuration de votre application.
- Logger (journal de logs) : un unique point d'entrée pour écrire les messages de log.
- Cache applicatif : un dict partagé qui stocke des données temporaires.
- Pool de threads ou de connexions : un gestionnaire unique qui distribue les ressources.
L'idée fondamentale est que certains objets n'ont de sens que s'ils existent en un seul exemplaire. Créer plusieurs instances de ces objets pourrait entraîner des incohérences, des conflits d'accès ou un gaspillage de ressources.
Implémentation avec __new__
La méthode la plus classique pour implémenter un Singleton en Python consiste à surcharger la méthode __new__ de votre classe. Cette méthode spéciale est appelée avant __init__ et contrôle la création de l'instance.
class Singleton:
_instance = None
def __new__(cls, *args, **kwargs):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
def __init__(self, valeur=None):
# Attention : __init__ est appelé à chaque appel de Singleton()
if valeur is not None:
self.valeur = valeur
# Utilisation
a = Singleton("premier")
b = Singleton("second")
print(a is b) # True
print(a.valeur) # second
print(id(a) == id(b)) # TrueAnalysons ce qui se passe étape par étape :
- Lors du premier appel
Singleton("premier"),_instanceestNone, donc__new__crée une nouvelle instance. - Ensuite,
__init__est appelé et affecteself.valeur = "premier". - Lors du second appel
Singleton("second"),_instanceexiste déjà, donc__new__retourne la même instance. __init__est à nouveau appelé, écrasantself.valeuravec"second".
Attention : avec cette implémentation, __init__ est appelé à chaque instanciation. Si vous y placez de la logique d'initialisation, elle sera exécutée plusieurs fois. Utilisez un flag pour contrôler cela.
Version protégée avec flag d'initialisation
Pour éviter que __init__ ne soit exécuté plusieurs fois, vous pouvez ajouter un attribut de contrôle :
class Singleton:
_instance = None
_initialized = False
def __new__(cls, *args, **kwargs):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
def __init__(self, valeur=None):
if not self._initialized:
self.valeur = valeur
self._initialized = True
a = Singleton("premier")
b = Singleton("second")
print(a.valeur) # premier
print(b.valeur) # premier (même instance, non réinitialisée)Cette fois, la valeur "premier" est conservée car __init__ ne s'exécute qu'une seule fois.
Implémentation avec un décorateur
Une approche plus pythonique consiste à utiliser un décorateur. Ce décorateur encapsule n'importe quelle classe et la transforme en Singleton, sans modifier son code interne :
def singleton(cls):
instances = {}
def get_instance(*args, **kwargs):
if cls not in instances:
instances[cls] = cls(*args, **kwargs)
return instances[cls]
return get_instance
@singleton
class Configuration:
def __init__(self, fichier="config.yml"):
self.fichier = fichier
self.parametres = self._charger()
def _charger(self):
# Simulation du chargement
return {"debug": True, "version": "1.0"}
# Utilisation
config1 = Configuration()
config2 = Configuration("autre_config.yml")
print(config1 is config2) # True
print(config1.fichier) # config.yml
print(config2.parametres) # {'debug': True, 'version': '1.0'}Cette solution est élégante car elle sépare la logique du Singleton de la logique métier de la classe. Le décorateur stocke les instances dans un dict local, ce qui permet même de gérer plusieurs classes Singleton indépendantes.
Implémentation avec une métaclasse
Pour les cas les plus avancés, vous pouvez utiliser une métaclasse. Une métaclasse contrôle la création des classes elles-mêmes, ce qui en fait un outil puissant pour le pattern Singleton :
class SingletonMeta(type):
_instances = {}
def __call__(cls, *args, **kwargs):
if cls not in cls._instances:
instance = super().__call__(*args, **kwargs)
cls._instances[cls] = instance
return cls._instances[cls]
class Logger(metaclass=SingletonMeta):
def __init__(self):
self.messages = []
def log(self, message):
self.messages.append(message)
print(f"[LOG] {message}")
# Utilisation
logger1 = Logger()
logger1.log("Démarrage de l'application")
logger2 = Logger()
logger2.log("Connexion utilisateur")
print(logger1 is logger2) # True
print(len(logger1.messages)) # 2
print(logger1.messages) # ['Démarrage de l\'application', 'Connexion utilisateur']La métaclasse SingletonMeta intercepte l'appel __call__ (déclenché quand vous écrivez Logger()) et vérifie si une instance existe déjà. Cette approche est la plus robuste et fonctionne parfaitement avec l'héritage.
Implémentation avec un module Python
Il existe une approche encore plus simple, propre à Python : utiliser un module comme Singleton. En Python, les modules sont des objets à instance unique par nature. Quand vous importez un module, Python le charge une seule fois et le met en cache.
# fichier: mon_singleton.py
class _Configuration:
def __init__(self):
self.debug = True
self.version = "1.0"
self.base_url = "https://example.com"
# Instance unique au niveau du module
config = _Configuration()
def get_parametre(cle):
return getattr(config, cle, None)# fichier: main.py
from mon_singleton import config, get_parametre
print(config.debug) # True
print(get_parametre("version")) # 1.0
# Partout dans votre code, config est la même instance
config.debug = FalseCette approche est souvent recommandée par la communauté Python car elle est simple, lisible et idiomatique. Pas besoin de class complexe ni de métaclasse.
Comparaison des différentes approches
| Méthode | Complexité | Héritage | Thread-safe | Pythonique |
|---|---|---|---|---|
__new__ | Moyenne | ⚠️ Partiel | ❌ Non | ✅ Oui |
| Décorateur | Faible | ❌ Non | ❌ Non | ✅ Oui |
| Métaclasse | Élevée | ✅ Oui | ❌ Non | ⚠️ Avancé |
| Module | Très faible | N/A | ✅ Oui* | ✅✅ Très |
* Le mécanisme d'import de Python est thread-safe par défaut grâce au GIL.
Singleton thread-safe
Si votre application est multi-thread, les implémentations classiques du Singleton ne sont pas sûres. Deux threads pourraient passer la vérification if _instance is None simultanément. Voici une version thread-safe avec un verrou :
import threading
class SingletonThreadSafe:
_instance = None
_lock = threading.Lock()
def __new__(cls, *args, **kwargs):
if cls._instance is None:
with cls._lock:
# Double vérification (double-checked locking)
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
def __init__(self):
pass
# Test avec plusieurs threads
def creer_instance():
instance = SingletonThreadSafe()
print(f"Thread {threading.current_thread().name}: {id(instance)}")
threads = [threading.Thread(target=creer_instance) for _ in range(10)]
for t in threads:
t.start()
for t in threads:
t.join()Le double-checked locking est une technique d'optimisation : le premier if évite d'acquérir le verrou inutilement si l'instance existe déjà, tandis que le second if (à l'intérieur du with) assure la sécurité thread.
Exemple concret : gestionnaire de base de données
Voyons un exemple réaliste d'utilisation du Singleton pour gérer une connexion à une base de données :
import sqlite3
import threading
class DatabaseManager(metaclass=SingletonMeta):
def __init__(self, db_path="app.db"):
self.db_path = db_path
self._connection = None
self._lock = threading.Lock()
@property
def connection(self):
if self._connection is None:
self._connection = sqlite3.connect(self.db_path)
return self._connection
def executer(self, requete, params=None):
with self._lock:
curseur = self.connection.cursor()
if params:
curseur.execute(requete, params)
else:
curseur.execute(requete)
self.connection.commit()
return curseur
def recuperer(self, requete, params=None):
curseur = self.executer(requete, params)
return curseur.fetchall()
def fermer(self):
if self._connection:
self._connection.close()
self._connection = None
# Utilisation partout dans l'application
db = DatabaseManager("mon_application.db")
db.executer("CREATE TABLE IF NOT EXISTS utilisateurs (id INTEGER PRIMARY KEY, nom TEXT)")
db.executer("INSERT INTO utilisateurs (nom) VALUES (?)", ("Alice",))
# Ailleurs dans le code, même instance
db2 = DatabaseManager()
resultats = db2.recuperer("SELECT * FROM utilisateurs")
print(resultats) # [(1, 'Alice')]Ici, peu importe où vous instanciez DatabaseManager() dans votre code, vous obtenez toujours la même connexion. Cela évite d'ouvrir plusieurs connexions inutiles et centralise la gestion de la base de données.
Bonnes pratiques et mises en garde
Quand utiliser le Singleton
- Vous avez besoin d'un point d'accès global unique à une ressource.
- La création de l'objet est coûteuse (connexion réseau, lecture de fichier).
- L'objet doit maintenir un état cohérent partagé entre plusieurs parties du code.
Quand éviter le Singleton
- Tests unitaires : le Singleton introduit un état global qui rend les tests difficiles à isoler. Préférez l'injection de dépendances.
- Couplage fort : les classes qui dépendent d'un Singleton sont couplées à son implémentation.
- Violation du principe de responsabilité unique : le Singleton gère à la fois sa logique métier et son cycle de vie.
- Pas besoin d'état : si votre classe n'a pas d'état, utilisez simplement des fonctions au niveau du module.
Le Singleton est parfois qualifié d'anti-pattern dans la communauté Python. Avant de l'utiliser, demandez-vous si un simple module ou l'injection de dépendances ne serait pas plus adapté.
Conseils pour une bonne implémentation
- Préférez l'approche module pour les cas simples — c'est la manière la plus pythonique.
- Utilisez la métaclasse si vous avez besoin d'héritage ou d'une structure orientée objet stricte.
- Protégez les accès concurrents avec un verrou (
threading.Lock) en environnement multi-thread. - Documentez clairement que votre classe est un Singleton — ajoutez une docstring explicite.
- Prévoyez une méthode de réinitialisation pour faciliter les tests unitaires.
class Singleton:
"""Classe Singleton — une seule instance est créée.
Utilisez Singleton.reset() dans les tests pour réinitialiser l'instance.
"""
_instance = None
def __new__(cls, *args, **kwargs):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
@classmethod
def reset(cls):
"""Réinitialise l'instance (utile pour les tests)."""
cls._instance = NoneQuestions fréquentes
Quelle est la différence entre un Singleton et une variable globale ?
Une variable global est simplement une valeur accessible partout, sans aucun contrôle sur sa création ou sa modification. Le Singleton, lui, encapsule la logique de création et garantit qu'une seule instance existe. Il offre un contrôle strict via la classe elle-même, tandis qu'une variable globale peut être écrasée à tout moment.
Le Singleton fonctionne-t-il avec l'héritage en Python ?
Cela dépend de l'implémentation. Avec la méthode __new__, l'héritage peut poser problème car l'attribut _instance est partagé. Avec une métaclasse, chaque sous-classe aura sa propre entrée dans le dictionnaire _instances, ce qui permet un Singleton par classe dans la hiérarchie d'héritage.
Le Singleton est-il thread-safe en Python ?
Non, les implémentations classiques ne sont pas thread-safe. Même si le GIL (Global Interpreter Lock) offre une certaine protection, il ne garantit pas l'atomicité de la vérification et de la création. Pour un Singleton thread-safe, utilisez un threading.Lock avec le pattern de double-checked locking, ou optez pour l'approche par module qui est naturellement thread-safe.
Comment apprendre à maîtriser les design patterns en Python ?
Les design patterns comme le Singleton deviennent naturels avec la pratique et une solide compréhension de la programmation orientée objet en Python. Nous vous recommandons de suivre notre formation Python sur Believemy, qui couvre en profondeur les classes, les métaclasses et les bonnes pratiques de conception logicielle. Vous y apprendrez à choisir le bon pattern pour chaque situation concrète.