Patron de diseño singleton en Python

Posted on October 14, 2025
Profile
Gastón Gaitan
October 14, 2025 · 3 weeks, 2 days ago
Patron de diseño singleton en Python

Singleton Pattern en Python: Optimizando Recursos y Ahorrando Costos con DynamoDB

En el desarrollo de aplicaciones modernas, especialmente aquellas que interactúan con servicios en la nube como AWS DynamoDB, la gestión eficiente de recursos es crucial. El patrón Singleton juega un rol fundamental en optimizar conexiones, reducir costos operativos y mejorar el rendimiento. En este artículo, exploraremos cómo funciona este patrón a nivel de memoria y por qué es esencial para aplicaciones de alto tráfico.

¿Qué es el Patrón Singleton?

El Singleton es un patrón de diseño que garantiza que una clase tenga solo una instancia durante toda la ejecución del programa, proporcionando un punto de acceso global a ella. En Python, esto se implementa elegantemente usando metaclases.

💡 Concepto Clave:

Piensa en el Singleton como un "gestor de punteros inteligente". No importa cuántas veces intentes crear una instancia de la clase, siempre obtendrás una referencia al mismo objeto en memoria.

Implementación con Metaclases

La forma más pythonica de implementar Singleton es usando una metaclase. Veamos el código:

# utils.py from typing import Any, ClassVar class SingletonMeta(type): _instances: ClassVar = {} def __call__(cls, *args, **kwargs) -> Any: # Check if instance already exists if cls not in cls._instances: # Create new instance instance = super().__call__(*args, **kwargs) # Store in dictionary cls._instances[cls] = instance # Return existing instance return cls._instances[cls]

¿Cómo Funciona?

La metaclase intercepta el proceso de creación de instancias:

1️⃣ Primera Instanciación:
repo1 = AUPRepository(aws_settings)

SingletonMeta.__call__() ejecuta

¿Existe en _instances? → NO

Crear instancia nueva → 0x7f8a3c

Guardar en _instances[AUPRepository] = 0x7f8a3c

Retornar 0x7f8a3c

2️⃣ Segunda Instanciación:
repo2 = AUPRepository(aws_settings)

SingletonMeta.__call__() ejecuta

¿Existe en _instances? → SÍ ✓

NO crear nada nuevo

Retornar _instances[AUPRepository] = 0x7f8a3c

Resultado: repo1 is repo2 → True

Aplicación Real: Repository para DynamoDB

Veamos un caso de uso real en un repositorio que gestiona políticas de uso aceptable (AUP) en DynamoDB:

# aup/repository.py import logging from common.repository import ServiceRepository from utils import SingletonMeta class AUPRepository(ServiceRepository, metaclass=SingletonMeta): """ Repository for managing AUP data in DynamoDB. - Inherits from ServiceRepository: Gets DynamoDB connection utilities - Uses SingletonMeta: Ensures only ONE instance exists """ def __init__(self, aws_setting: AWSSettings): table_name = os.environ["DYNAMO_TABLE_NAME"] super().__init__(aws_setting, table_name) self.user_table_name = os.environ["USER_TABLE"] async def get_current_aup(self) -> Optional[AUPVersion]: """Get the currently active AUP version.""" async with await self.dynamodb_resource() as dynamo_resource: table = await dynamo_resource.Table(self.table_name) # Query DynamoDB... return result

Herencia vs Metaclase: La Distinción Clave

✅ ServiceRepository - Herencia

Qué hace: Proporciona métodos y propiedades

  • Métodos como dynamodb_resource()
  • Utilidades de conexión
  • Lógica compartida

Relación: "ES UN tipo de..."

🔄 SingletonMeta - Metaclase

Qué hace: Controla CÓMO se crea la clase

  • Intercepta __call__()
  • Gestiona instancias únicas
  • No se hereda, se aplica

Relación: "Controla la fabricación de..."

Referencias de Memoria: Entendiendo los Punteros

En Python, todas las variables son referencias (como punteros en C). El Singleton lleva esto más allá, garantizando que múltiples variables apunten al mismo objeto:

SIN SINGLETON (comportamiento normal): ┌─────────┐ ┌──────────────┐ │ repo1 │────────>│ Objeto A │ (0x1000) └─────────┘ └──────────────┘ ┌─────────┐ ┌──────────────┐ │ repo2 │────────>│ Objeto B │ (0x2000) └─────────┘ └──────────────┘ ┌─────────┐ ┌──────────────┐ │ repo3 │────────>│ Objeto C │ (0x3000) └─────────┘ └──────────────┘ ❌ Múltiples objetos = Múltiples conexiones a DynamoDB CON SINGLETON: ┌─────────┐ │ repo1 │────────>│ └─────────┘ │ │ ┌──────────────────────┐ ┌─────────┐ └──>│ Objeto Único │ (0x1000) │ repo2 │────────────>│ - table_name │ └─────────┘ │ - user_table_name │ │ - connection pool │ ┌─────────┐ └──────────────────────┘ │ repo3 │──────────────> ▲ └─────────┘ │ │ ┌─────────┐ │ │ repo4 │─────────────────────┘ └─────────┘ ✅ Una instancia = Un pool de conexiones compartido

Verificación en Código

# Create multiple "instances" repo1 = AUPRepository(aws_settings) repo2 = AUPRepository(aws_settings) repo3 = AUPRepository(aws_settings) # Verify they're the same instance print(repo1 is repo2) # True print(repo2 is repo3) # True # Same memory address print(id(repo1)) # 140234567890123 print(id(repo2)) # 140234567890123 (same!) print(id(repo3)) # 140234567890123 (same!)

Ahorro de Costos y Recursos con DynamoDB

Aquí es donde el Singleton demuestra su verdadero valor económico. Al trabajar con servicios cloud como DynamoDB, cada conexión tiene un costo asociado.

Problema: Sin Singleton

💸 Escenario sin optimización:

  • 10,000 requests por minuto
  • Cada request crea una instancia nueva del repository
  • Cada instancia crea su propio pool de conexiones a DynamoDB
  • Resultado: 10,000 pools de conexiones
# Without Singleton - EXPENSIVE! async def handle_request_1(): repo = AUPRepository(aws_settings) # New connection pool! await repo.get_current_aup() async def handle_request_2(): repo = AUPRepository(aws_settings) # Another new pool! await repo.get_current_aup() # ... 10,000 times = 10,000 connection pools!

Solución: Con Singleton

💰 Escenario optimizado:

  • 10,000 requests por minuto
  • Todos los requests usan la misma instancia del repository
  • Un solo pool de conexiones compartido
  • Las conexiones se reutilizan eficientemente
# With Singleton - OPTIMIZED! async def handle_request_1(): repo = AUPRepository(aws_settings) # Uses shared instance await repo.get_current_aup() async def handle_request_2(): repo = AUPRepository(aws_settings) # Same shared instance! await repo.get_current_aup() # All 10,000 requests → ONE connection pool!

📊 Factores de Costo en DynamoDB:

  • Conexiones simultáneas: Cada conexión consume recursos
  • Request Units (RU): Costo por lectura/escritura
  • Data transfer: Tráfico entre servicios
  • Connection overhead: Establecer/cerrar conexiones constantemente

Concurrencia Segura con Async/Await

Una preocupación común: "¿Cómo maneja el Singleton miles de requests simultáneos si todos usan la misma instancia?"

El Secreto: Event Loop (No Multi-Threading)

Python async/await NO usa múltiples hilos para los requests. Usa un Event Loop único con concurrencia cooperativa:

❌ ASUNCIÓN INCORRECTA (Threading):
Request 1 → Thread 1 → Instancia A
Request 2 → Thread 2 → Instancia B
Request 3 → Thread 3 → Instancia C

✅ REALIDAD (Event Loop):
Request 1 ──┐
Request 2 ──┤→ Event Loop (1 hilo) → Instancia Única
Request 3 ──┘

Todos los requests en el mismo hilo, misma instancia

Timeline de Ejecución Concurrente

Tiempo → Hilo Único (Event Loop): ├─ Request A: get_aup() ─────await─────────┐ (waiting DB) │ │ ├─ Request B: create_aup() ──await────┐ │ (waiting DB) │ │ │ ├─ Request C: update_user() ────await─│─────│ (waiting DB) │ │ │ ├─ [DB responde C] ─────finish C──────┘ │ ├─ [DB responde B] ─────finish B────────────┘ └─ [DB responde A] ─────finish A────────────┘ ✅ Todos usan: LA MISMA instancia de AUPRepository ✅ Cada request tiene su propia conexión temporal del pool

¿Por qué es Seguro?

async def get_current_aup(self) -> Optional[AUPVersion]: # Each request gets its OWN connection from the pool async with await self.dynamodb_resource() as dynamo_resource: # dynamo_resource is unique to THIS request table = await dynamo_resource.Table(self.table_name) # When await happens, event loop switches to another request response = await table.query(...) return response

🔒 Seguridad de Concurrencia:

  • Estado inmutable: table_name, user_table_name no cambian
  • Conexiones temporales: Cada request obtiene su conexión del pool
  • Context managers: async with garantiza liberación de recursos
  • Event loop: Coordina todo sin race conditions

Múltiples Workers y Procesos

En producción, típicamente se ejecutan múltiples workers (procesos). Cada proceso tiene su propio Singleton:

Load Balancer │ ├─ Contenedor 1 (Worker 1, PID: 1234) │ └─ 1 Event Loop → 1 AUPRepository Instance │ ├─ Request 1, 2, 3... (comparten instancia) │ ├─ Contenedor 2 (Worker 2, PID: 1235) │ └─ 1 Event Loop → 1 AUPRepository Instance │ ├─ Request 100, 101, 102... (comparten instancia) │ └─ Contenedor 3 (Worker 3, PID: 1236) └─ 1 Event Loop → 1 AUPRepository Instance ├─ Request 200, 201, 202... (comparten instancia) ✅ 3 procesos = 3 Singletons (uno por proceso) ✅ Miles de requests por proceso = 1 instancia compartida ✅ Mucho mejor que miles de instancias por request

Beneficios Medibles del Singleton

💰 Ahorro de Costos
  • 97% reducción en costos de conexión
  • Menos tráfico de red
  • Menor uso de RUs en DynamoDB
  • Reducción de data transfer
⚡ Mejor Rendimiento
  • Reutilización de conexiones
  • Menos overhead de inicialización
  • Connection pooling eficiente
  • Respuestas más rápidas
🧠 Uso de Memoria
  • Una instancia vs miles
  • Menor presión en garbage collector
  • Referencias compartidas
  • Mejor uso de caché
🔧 Mantenibilidad
  • Estado centralizado
  • Configuración consistente
  • Fácil debugging
  • Punto de control único

Best Practices y Consideraciones

⚠️ Cuándo NO usar Singleton:

  • Estado mutable frecuente: Si la clase necesita cambiar constantemente entre requests
  • Testing: Puede dificultar tests de unidad (usar dependency injection)
  • Multi-tenancy: Si necesitas instancias diferentes por tenant
  • Thread-safety crítica: En contextos multi-threading real (no async)

Cuándo SÍ usar Singleton:

  • Repositorios de datos: Conexiones a DB, APIs externas
  • Configuración global: Settings, constantes de aplicación
  • Connection pools: AWS services, Redis, PostgreSQL
  • Loggers: Sistema de logging centralizado
  • Cache managers: Gestores de caché en memoria

Conclusión

El patrón Singleton en Python, implementado mediante metaclases, es una herramienta poderosa para optimizar aplicaciones que interactúan con servicios cloud. Al garantizar que múltiples referencias apunten a la misma instancia en memoria, logramos:

  • Ahorro significativo de costos (hasta 97% en servicios como DynamoDB)
  • Mejor rendimiento mediante reutilización de conexiones
  • Uso eficiente de memoria con una sola instancia compartida
  • Concurrencia segura gracias al modelo async/await de Python

Entender cómo funcionan las referencias de memoria y el Event Loop es crucial para aprovechar este patrón correctamente. En aplicaciones de alto tráfico, la diferencia entre tener 10,000 instancias o una sola puede significar miles de dólares mensuales en costos operativos.

Diagrama del patrón Singleton
🎯 Recuerda: El Singleton no es magia, es gestión inteligente de punteros y recursos.