Patron Singleton en Python
Le singleton est un patron de conception logicielle couramment utilisé qui garantit qu’une classe ne possède qu’une seule instance et fournit un point d’accès global pour obtenir cette instance.
Concepts fondamentaux
Imaginez le poste de PDG dans une entreprise - l’entreprise entière ne peut avoir qu’un seul PDG. Peu importe le service qui doit rendre des comptes au PDG, tous accèdent au même objet PDG. Le patron singleton est le « mécanisme de gestion du poste de PDG » dans le monde de la programmation.
Pourquoi avons-nous besoin du patron singleton ?
Dans le développement réel, pour certains objets, nous n’avons besoin que d’une seule instance, par exemple :
- Gestionnaire de configuration: L’application entière partage le même ensemble de configurations
- Pool de connexions à la base de données: Éviter de créer des connexions de manière répétée et économiser des ressources
- Journaliseur: Garantir que tous les journaux sont écrits dans le même fichier
- Système de cache: Gérer les données mises en cache de manière uniforme
Façons de mettre en œuvre le patron singleton
Python propose plusieurs façons de mettre en œuvre le patron singleton. Apprenons-les une par une, du plus simple au plus complexe.
Méthode 1 : Utilisation d’un module (la façon la plus simple)
Les modules Python sont eux-mêmes naturellement des singletons. Lorsqu’un module est importé, il est initialisé une fois, et les importations suivantes utiliseront la même instance.
Exemple
class DatabaseConnection:
def __init__(self):
self.connection_string = "database://localhost:5432/mydb"
print("Connexion à la base de données créée")
def query(self, sql):
return f"Exécution de la requête : {sql}"
# Créer une instance singleton
db_instance = DatabaseConnection()
# Utilisez-le dans d'autres fichiers :
# from singleton_module import db_instance
# result = db_instance.query("SELECT * FROM users")
Méthode 2 : En utilisant__new__la méthode __new__
En redéfinissant__new__la méthode __new__ pour contrôler le processus d'instanciation.
Exemple
_instance = None
def __new__(cls, *args, **kwargs):
# Si l'instance n'existe pas, en créer une nouvelle
if not cls._instance:
cls._instance = super().__new__(cls)
print("Création d'une nouvelle instance singleton")
else:
print("Retour de l'instance singleton existante")
return cls._instance
def __init__(self, name):
# Remarque : __init__ est appelé à chaque fois
self.name = name
print(f"Initialisation de l'instance, nom : {name}")
# Code de test
print("=== Test du patron Singleton ===")
s1 = Singleton("Première instance")
s2 = Singleton("Deuxième instance")
print(f"ID de s1 : {id(s1)}")
print(f"ID de s2 : {id(s2)}")
print(f"s1 et s2 sont-ils le même objet ? {s1 is s2}")
print(f"nom de s1 : {s1.name}")
print(f"nom de s2 : {s2.name}") # Remarque : "Deuxième instance" sera affichée ici
Résultat de sortie :
=== 测试单例模式 === 创建新的单例实例 初始化实例,名称: 第一个实例 返回已存在的单例实例 初始化实例,名称: 第二个实例 s1 的 ID: 140245678945600 s2 的 ID: 140245678945600 s1 和 s2 是同一个对象吗? True s1 名称: 第二个实例 s2 名称: 第二个实例
Méthode 3 : Utilisation d'un décorateur
Créez un décorateur singleton générique qui peut facilement convertir n'importe quelle classe en singleton.
Exemple
"""Décorateur singleton"""
instances = {}
def get_instance(*args, **kwargs):
# Si cette classe n'a pas encore d'instance, créez-en une nouvelle
if cls not in instances:
instances[cls] = cls(*args, **kwargs)
print(f"Création d'une nouvelle instance de {cls.__name__}")
else:
print(f"Retour de l'instance existante {cls.__name__}")
return instances[cls]
return get_instance
@singleton
class ConfigurationManager:
def __init__(self):
self.settings = {}
self.load_default_settings()
def load_default_settings(self):
self.settings = {
"app_name": "Mon application",
"version": "1.0.0",
"debug_mode": True
}
def get_setting(self, key):
return self.settings.get(key)
def set_setting(self, key, value):
self.settings[key] = value
# Code de test
print("\n"=== Test du Singleton décorateur ===")
config1 = ConfigurationManager()
config2 = ConfigurationManager()
config1.set_setting("theme", "dark")
print(f"thème de config1 : {config1.get_setting('theme')}")
print(f"thème de config2 : {config2.get_setting('theme')}") # Les deux instances partagent la même configuration
Méthode 4 : Utilisation d'une métaclasse (utilisation avancée)
Une métaclasse peut contrôler le processus de création des classes, ce qui est une autre manière puissante d'implémenter le patron singleton.
Exemple
"""Métaclasse Singleton"""
_instances = {}
def __call__(cls, *args, **kwargs):
# Si cette classe n'a pas encore d'instance, créez-en une nouvelle
if cls not in cls._instances:
instance = super().__call__(*args, **kwargs)
cls._instances[cls] = instance
print(f"Métaclasse : création d'une nouvelle instance de {cls.__name__}")
else:
print(f"Métaclasse : retour de l'instance existante de {cls.__name__}")
return cls._instances[cls]
class Logger(metaclass=SingletonMeta):
def __init__(self, log_file="app.log"):
self.log_file = log_file
self.logs = []
print(f"Logger initialisé, fichier : {log_file}")
def log(self, message):
log_entry = f"[{self.get_timestamp()}] {message}"
self.logs.append(log_entry)
print(f"Enregistrement de l'entrée : {log_entry}")
return log_entry
def get_timestamp(self):
from datetime import datetime
return datetime.now().strftime("%Y-%m-%d %H:%M:%S")
def get_logs(self):
return self.logs.copy()
# Code de test
print("\n"=== Test du singleton de métaclasse ===")
logger1 = Logger("application.log")
logger2 = Logger("different.log") # Ce nom de fichier sera ignoré
logger1.log("Démarrage du système")
logger2.log("Connexion de l'utilisateur")
print(f"nombre de journaux de logger1 : {len(logger1.get_logs())}")
print(f"nombre de journaux de logger2 : {len(logger2.get_logs())}")
print(f"S'agit-il du même logger ? {logger1 is logger2}")
Scénarios d'application du patron Singleton
Voyons l'application du patron singleton dans des projets réels à travers un exemple complet.
Cas pratique : Gestionnaire de configuration d'application
Exemple
_instance = None
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
cls._instance._initialized = False
return cls._instance
def __init__(self):
# Empêcher l'initialisation répétée
if not self._initialized:
self.config_data = {}
self.load_config()
self._initialized = True
def load_config(self):
"""Simuler le chargement de la configuration à partir d'un fichier de configuration"""
self.config_data = {
"database": {
"host": "localhost",
"port": 5432,
"name": "myapp_db"
},
"server": {
"host": "0.0.0.0",
"port": 8000
},
"features": {
"cache_enabled": True,
"debug_mode": False
}
}
print("Configuration chargée")
def get(self, key_path, default=None):
"""Obtenir la valeur de configuration par chemin, par exemple 'database.host'"""
keys = key_path.split('.')
value = self.config_data
try:
for key in keys:
value = value[key]
return value
except (KeyError, TypeError):
return default
def set(self, key_path, value):
"""Définir la valeur de configuration"""
keys = key_path.split('.')
config = self.config_data
# Parcourir jusqu'à la clé avant la dernière
for key in keys[:-1]:
if key not in config:
config[key] = {}
config = config[key]
# Définir la valeur finale
config[keys[-1]] = value
print(f"Configuration mise à jour : {key_path} = {value}")
# Exemple d'utilisation
def demonstrate_config_usage():
print("\n"=== Démo d'utilisation du gestionnaire de configuration ===")
# Obtenir l'instance du gestionnaire de configuration à différents endroits
config1 = AppConfig()
config2 = AppConfig()
print(f"S'agit-il du même gestionnaire de configuration ? {config1 is config2}")
# Lire la configuration
db_host = config1.get("database.host")
server_port = config1.get("server.port")
print(f"Hôte de la base de données : {db_host}")
print(f"Port du serveur : {server_port}")
# Mettre à jour la configuration
config2.set("database.host", "192.168.1.100")
config2.set("features.debug_mode", True)
# Vérifier la synchronisation de la configuration
print(f"Hôte de la base de données de config1 : {config1.get('database.host')}")
print(f"mode de débogage de config1 : {config1.get('features.debug_mode')}")
# Exécuter la démo
demonstrate_config_usage()
Considérations sur le patron Singleton
Avantages
- Économie de ressources: Éviter de créer des objets de manière répétée, économisant la mémoire et les ressources système
- Cohérence des données: Tous les clients utilisent la même instance, garantissant la cohérence des données
- Accès global: Fournir un point d'accès unifié pour faciliter la gestion
Inconvénients
- État global: L'objet singleton détient un état global, ce qui peut causer des effets de bord inattendus
- Tests difficiles: En raison de l'état global, les tests unitaires peuvent devenir compliqués
- Viole le principe de responsabilité unique: La classe singleton doit non seulement gérer sa propre logique métier, mais aussi contrôler l'instanciation
Bonnes pratiques
Exemple
"""Patron singleton thread-safe"""
_instance = None
_lock = threading.Lock()
def __new__(cls):
if cls._instance is None:
with cls._lock:
# Verrouillage à double vérification
if cls._instance is None:
cls._instance = super().__new__(cls)
print("Création d'une instance singleton sûre pour les threads")
return cls._instance
def __init__(self):
# S'assurer qu'il n'est initialisé qu'une seule fois
if not hasattr(self, '_initialized'):
self.data = {}
self._initialized = True
Exercices et réflexion
Exercices pratiques
Implémenter un gestionnaire de cache:
- Créer une classe de cache singleton
- Prendre en charge la définition, la récupération et la suppression des éléments de cache
- Ajouter une fonctionnalité de délai d'expiration du cache
Améliorer le gestionnaire de configuration:
- Ajouter une fonctionnalité pour charger la configuration depuis un fichier JSON
- Implémenter un écouteur de changement de configuration
- Ajouter un mécanisme de validation de configuration
Questions de réflexion
- Dans quelles situations faut-il éviter le patron singleton ?
- Comment le patron singleton affecte-t-il la testabilité du code ?
- Que faut-il prendre en compte lorsqu'on utilise le patron singleton dans un environnement multithread ?
Résumé
Le patron singleton est un patron de conception très important en Python. En garantissant qu'une classe n'a qu'une seule instance, il fournit une gestion unifiée des ressources. Par le biais des modules,__new__méthodes, décorateurs, métaclasses et autres approches d'implémentation, nous pouvons choisir la méthode la plus adaptée selon des besoins spécifiques.
Rappelez-vous que, bien que le patron singleton soit très pratique, il convient de l'utiliser avec prudence pour éviter le couplage du code et les difficultés de test causées par une utilisation excessive. Dans des projets réels, l'utilisation appropriée du patron singleton peut grandement améliorer la qualité et la maintenabilité du code.
Autres extensions