Estrategias de Implementación del Patrón Singleton en Python y su Optimización

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.

Etiquetas: Python singleton-pattern design-patterns Multithreading metaprogramming

Publicado el 6-20 21:07