El patrón Singleton es un diseño creacional que garantiza que una clase tenga una única instancia en todo el ciclo de vida de una aplicación, proporcionendo un punto de acceso global a ella. Es extremadamente útil en escenarios donde compartir recursos es crítico, como en la gestión de conexiones a bases de datos, sistemas de logs o el manejo de configuraciones globales para evitar el consumo excesivo de memoria.
1. Singleton basado en Módulos
En Python, los módulos funcionan como singletons de forma nativa. Cuando un módulo se importa por primera vez, Python lo compila y lo almacena en caché. Cualquier importación posterior devolverá la misma refeerncia.
# config_global.py
class GestorConfiguracion:
def __init__(self):
self.ajustes = {}
instancia_config = GestorConfiguracion()
# En otro archivo
# from config_global import instancia_config
2. Uso de Decoradores
Podemos envolver una clase con un decorador para gestionar sus instancias mediante un diccionario interno, donde la clave es la clasee y el valor es su única instancia.
def singleton(cls):
instancias = {}
def obtener_instancia(*args, **kwargs):
if cls not in instancias:
instancias[cls] = cls(*args, **kwargs)
return instancias[cls]
return obtener_instancia
@singleton
class ServidorAPI:
def __init__(self, endpoint):
self.endpoint = endpoint
# api1 y api2 apuntarán al mismo objeto
api1 = ServidorAPI("https://api.ejemplo.com")
api2 = ServidorAPI("https://api.otro.com")
3. Implementación con Métodos de Clase y Seguridad en Hilos
Una implementación común utiliza un método estático o de clase. Sin embargo, en entornos multihilo, si dos hilos intentan instanciar la clase al mismo tiempo, podrían crearse dos objetos distintos. Para evitar esto, utilizamos un objeto de bloqueo (Lock) y la técnica de "bloqueo de doble verificación" (Double-Checked Locking).
import threading
import time
class DatabaseProxy:
_instancia_unica = None
_candado = threading.Lock()
def __init__(self):
# Simulación de una operación costosa
time.sleep(0.5)
@classmethod
def get_proxy(cls, *args, **kwargs):
if not cls._instancia_unica:
with cls._candado:
if not cls._instancia_unica:
cls._instancia_unica = cls(*args, **kwargs)
return cls._instancia_unica
def ejecutar_hilo():
db = DatabaseProxy.get_proxy()
print(f"Instancia: {id(db)}")
# Prueba con múltiples hilos
for _ in range(5):
threading.Thread(target=ejecutar_hilo).start()
4. Sobrescribiendo el método __new__
Este es el enfoque más transparente para el usuario de la clase, ya que permite instanciar el objeto de la forma convencional Clase(), pero manteniendo la lógica de instancia única internamente.
import threading
class LoggerSistema:
_instancia = None
_lock_recurso = threading.Lock()
def __new__(cls, *args, **kwargs):
if not cls._instancia:
with cls._lock_recurso:
if not cls._instancia:
cls._instancia = super(LoggerSistema, cls).__new__(cls)
return cls._instancia
log_a = LoggerSistema()
log_b = LoggerSistema()
# print(log_a is log_b) -> True
5. Implementación mediante Metaclases
Las metaclases son las "clases de las clases". Al heredar de type y sobrescribir el método __call__, podemos controlar qué sucede exactamente cuando se invoca el nombre de la clase para crear un nuevo objeto.
import threading
class SingletonMeta(type):
_instancias_registradas = {}
_bloqueo_meta = threading.Lock()
def __call__(cls, *args, **kwargs):
if cls not in cls._instancias_registradas:
with cls._bloqueo_meta:
if cls not in cls._instancias_registradas:
# Llamada al método __call__ original de la clase superior
instancia = super(SingletonMeta, cls).__call__(*args, **kwargs)
cls._instancias_registradas[cls] = instancia
return cls._instancias_registradas[cls]
class ConexionCache(metaclass=SingletonMeta):
def __init__(self, ttl):
self.ttl = ttl
cache1 = ConexionCache(3600)
cache2 = ConexionCache(7200)
# Ambos objetos serán idénticos
Cada método tiene sus ventajas: el uso de módulos es el más simple y "pythónico", los decoradores son altamente reutilizables, y el método __new__ o las metaclases ofrecen una integración más profunda con el sistema de tipos de Python, siendo ideales para frameworks o librerías complejas.