Patrón Singleton en Python
El patrón Singleton es un patrón de diseño de software común que garantiza que una clase tenga una única instancia y proporciona un punto de acceso global para obtenerla.
Concepto central
Imagina el puesto de CEO de una empresa: solo puede haber un CEO en toda la empresa. Sin importar qué departamento necesite reportar al CEO, todos acceden al mismo objeto CEO. El patrón Singleton es el "mecanismo de gestión del puesto de CEO" en el mundo de la programación.
¿Por qué se necesita el patrón Singleton?
En el desarrollo real, hay objetos para los que solo necesitamos una única instancia, por ejemplo:
- Gestor de configuración: toda la aplicación comparte la misma configuración
- Pool de conexiones de base de datos: evita crear conexiones repetidamente y ahorra recursos
- Logger: asegura que todos los registros se escriban en el mismo archivo
- Sistema de caché: gestiona los datos de caché de manera unificada
Métodos de implementación del patrón Singleton
Python ofrece múltiples formas de implementar el patrón Singleton. Vamos a conocerlas una por una, de lo simple a lo complejo.
Método 1: Usar un módulo (la forma más sencilla)
Los módulos de Python son por sí mismos un patrón Singleton natural. Cuando un módulo se importa, se inicializa una vez; las importaciones posteriores usarán la misma instancia.
Ejemplo
class DatabaseConnection:
def __init__(self):
self.connection_string = "database://localhost:5432/mydb"
print("Conexión a la base de datos creada")
def query(self, sql):
return f"Ejecutando consulta: {sql}"
# Crear la instancia Singleton
db_instance = DatabaseConnection()
# Usar en otros archivos:
# from singleton_module import db_instance
# result = db_instance.query("SELECT * FROM users")
Método 2: Usar__new__Método
Sobrescribiendo__new__método para controlar el proceso de creación de la instancia.
Ejemplo
_instance = None
def __new__(cls, *args, **kwargs):
# Si la instancia no existe, crear una nueva instancia
if not cls._instance:
cls._instance = super().__new__(cls)
print("Creando nueva instancia Singleton")
else:
print("Devolviendo la instancia Singleton existente")
return cls._instance
def __init__(self, name):
# Nota: __init__ se llamará cada vez
self.name = name
print(f"Inicializando instancia, nombre: {name}")
# Código de prueba
print("=== Prueba del patrón Singleton ===")
s1 = Singleton("Primera instancia")
s2 = Singleton("Segunda instancia")
print(f"ID de s1: {id(s1)}")
print(f"ID de s2: {id(s2)}")
print(f"¿Son s1 y s2 el mismo objeto? {s1 is s2}")
print(f"Nombre de s1: {s1.name}")
print(f"Nombre de s2: {s2.name}") # Nota: aquí se mostrará "Segunda instancia"
Resultado de salida:
=== 测试单例模式 === 创建新的单例实例 初始化实例,名称: 第一个实例 返回已存在的单例实例 初始化实例,名称: 第二个实例 s1 的 ID: 140245678945600 s2 的 ID: 140245678945600 s1 和 s2 是同一个对象吗? True s1 名称: 第二个实例 s2 名称: 第二个实例
Método 3: Usar decoradores
Crea un decorador Singleton genérico que puede convertir fácilmente cualquier clase en un Singleton.
Ejemplo
"""Decorador Singleton"""
instances = {}
def get_instance(*args, **kwargs):
# Si la clase aún no tiene una instancia, crear una nueva instancia
if cls not in instances:
instances[cls] = cls(*args, **kwargs)
print(f"Creando nueva instancia de {cls.__name__}")
else:
print(f"Devolviendo instancia existente de {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": "Mi aplicación",
"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
# Código de prueba
print("\n"=== Prueba del Singleton con decorador ===")
config1 = ConfigurationManager()
config2 = ConfigurationManager()
config1.set_setting("theme", "dark")
print(f"Tema de config1: {config1.get_setting('theme')}")
print(f"Tema de config2: {config2.get_setting('theme')}") # Ambas instancias comparten la misma configuración
Método 4: Usar metaclases (uso avanzado)
Las metaclases pueden controlar el proceso de creación de clases, otra forma potente de implementar el patrón Singleton.
Ejemplo
"""Metaclase Singleton"""
_instances = {}
def __call__(cls, *args, **kwargs):
# Si la clase aún no tiene una instancia, crear una nueva instancia
if cls not in cls._instances:
instance = super().__call__(*args, **kwargs)
cls._instances[cls] = instance
print(f"Metaclase: creando nueva instancia de {cls.__name__}")
else:
print(f"Metaclase: devolviendo instancia existente 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 inicializado, archivo: {log_file}")
def log(self, message):
log_entry = f"[{self.get_timestamp()}] {message}"
self.logs.append(log_entry)
print(f"Registrando log: {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()
# Código de prueba
print("\n"=== Prueba del Singleton con metaclase ===")
logger1 = Logger("application.log")
logger2 = Logger("different.log") # Este nombre de archivo se ignorará
logger1.log("Inicio del sistema")
logger2.log("Inicio de sesión de usuario")
print(f"Cantidad de registros de logger1: {len(logger1.get_logs())}")
print(f"Cantidad de registros de logger2: {len(logger2.get_logs())}")
print(f"¿Es el mismo logger? {logger1 is logger2}")
Escenarios de aplicación del patrón Singleton
Veamos la aplicación del patrón Singleton en proyectos reales mediante un ejemplo completo.
Caso práctico: Gestor de configuración de la aplicación
Ejemplo
_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):
# Evitar inicialización repetida
if not self._initialized:
self.config_data = {}
self.load_config()
self._initialized = True
def load_config(self):
"""Simula cargar la configuración desde un archivo de configuración"""
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("Configuración cargada")
def get(self, key_path, default=None):
"""Obtiene el valor de configuración mediante una ruta, como '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):
"""Establece el valor de configuración"""
keys = key_path.split('.')
config = self.config_data
# Recorrer hasta la clave anterior a la última
for key in keys[:-1]:
if key not in config:
config[key] = {}
config = config[key]
# Establecer el valor final
config[keys[-1]] = value
print(f"Configuración actualizada: {key_path} = {value}")
# Ejemplo de uso
def demonstrate_config_usage():
print("\n"=== Demostración del gestor de configuración ===")
# Obtener instancias del gestor de configuración en diferentes lugares
config1 = AppConfig()
config2 = AppConfig()
print(f"¿Es el mismo gestor de configuración? {config1 is config2}")
# Leer configuración
db_host = config1.get("database.host")
server_port = config1.get("server.port")
print(f"Host de base de datos: {db_host}")
print(f"Puerto del servidor: {server_port}")
# Actualizar configuración
config2.set("database.host", "192.168.1.100")
config2.set("features.debug_mode", True)
# Verificar la sincronización de la configuración
print(f"Host de base de datos de config1: {config1.get('database.host')}")
print(f"Modo de depuración de config1: {config1.get('features.debug_mode')}")
# Ejecutar demostración
demonstrate_config_usage()
Consideraciones sobre el patrón Singleton
Ventajas
- Ahorro de recursos: evita crear objetos repetidamente, ahorra memoria y recursos del sistema
- Consistencia de datos: todos los clientes usan la misma instancia, lo que garantiza la coherencia de los datos
- Acceso global: proporciona un punto de acceso unificado, fácil de gestionar
Desventajas
- Estado global: el objeto Singleton mantiene un estado global, lo que puede causar efectos secundarios inesperados
- Dificultad de pruebas: debido al estado global, las pruebas unitarias pueden volverse complejas
- Violación del principio de responsabilidad única: la clase Singleton debe gestionar su propia lógica de negocio y también controlar la instanciación
Mejores prácticas
Ejemplo
"""Patrón Singleton seguro para subprocesos"""
_instance = None
_lock = threading.Lock()
def __new__(cls):
if cls._instance is None:
with cls._lock:
# Bloqueo de doble verificación
if cls._instance is None:
cls._instance = super().__new__(cls)
print("Creando instancia Singleton segura para subprocesos")
return cls._instance
def __init__(self):
# Asegurar que se inicialice solo una vez
if not hasattr(self, '_initialized'):
self.data = {}
self._initialized = True
Ejercicios y reflexión
Ejercicios prácticos
Implementar un gestor de caché:
- Crear una clase de caché Singleton
- Soportar establecer, obtener y eliminar elementos de caché
- Agregar función de tiempo de expiración de caché
Mejorar el gestor de configuración:
- Agregar funcionalidad para cargar configuración desde un archivo JSON
- Implementar un listener de cambios de configuración
- Agregar mecanismo de validación de configuración
Preguntas de reflexión
- ¿En qué situaciones se debe evitar el patrón Singleton?
- ¿Cómo afecta el patrón Singleton a la capacidad de prueba del código?
- ¿Qué se debe tener en cuenta al utilizar el patrón singleton en entornos multihilo?
Resumen
El patrón singleton es un patrón de diseño muy importante en Python. Al garantizar que una clase tenga una única instancia, proporciona una gestión unificada de los recursos. A través de módulos,__new__métodos, decoradores y metaclases, entre otras formas de implementación, podemos elegir el método más adecuado según las necesidades específicas.
Recuerda que, aunque el patrón singleton es muy práctico, también debe usarse con precaución para evitar el acoplamiento de código y las dificultades de prueba que causa el uso excesivo. En proyectos reales, un uso razonable del patrón singleton puede mejorar en gran medida la calidad y el mantenimiento del código.
Otras extensiones