Performance Patterns
Patrones para optimizar velocidad, memoria y escalabilidad
Los Performance Patterns abordan cuellos de botella comunes y desafíos de optimización, proporcionando metodologías probadas para mejorar el tiempo de ejecución, reducir el uso de memoria y escalar efectivamente.
En este capítulo exploraremos tres patrones fundamentales: Cache-Aside para gestionar datos frecuentemente accedidos, Memoization para evitar cálculos redundantes, y Lazy Loading para diferir la inicialización de recursos costosos.
- Python 3.12+
- Faker:
pip install faker - Redis:
pip install redis - Redis Server:
docker run --name myredis -p 6379:6379 redis
Cache-Aside Pattern
En situaciones donde los datos se leen con más frecuencia de lo que se actualizan, las aplicaciones usan una caché para optimizar el acceso repetido a información almacenada en una base de datos. El patrón Cache-Aside almacena datos frecuentemente accedidos en una caché, reduciendo la necesidad de consultar repetidamente la base de datos.
Ejemplos del Mundo Real
Casos de Uso
Flujo del Patrón
Implementación: Script de Población
import sqlite3 from pathlib import Path from random import randint import redis from faker import Faker fake = Faker() DB_PATH = Path(__file__).parent / Path("quotes.sqlite3") cache = redis.StrictRedis( host="localhost", port=6379, decode_responses=True ) def setup_db(): try: with sqlite3.connect(DB_PATH) as db: cursor = db.cursor() cursor.execute(""" CREATE TABLE quotes(id INTEGER PRIMARY KEY, text TEXT) """) db.commit() print("Table 'quotes' created") except Exception as e: print(e) def add_quotes(quotes_list): added = [] try: with sqlite3.connect(DB_PATH) as db: cursor = db.cursor() for quote_text in quotes_list: quote_id = randint(1, 100) quote = (quote_id, quote_text) cursor.execute( """INSERT OR IGNORE INTO quotes(id, text) VALUES(?, ?)""", quote ) added.append(quote) db.commit() except Exception as e: print(e) return added
Implementación: Operaciones Cache-Aside
import sqlite3 from pathlib import Path import redis CACHE_KEY_PREFIX = "quote" DB_PATH = Path(__file__).parent / Path("quotes.sqlite3") cache = redis.StrictRedis( host="localhost", port=6379, decode_responses=True ) def get_quote(quote_id: str) -> str: out = [] # Primero: Buscar en caché quote = cache.get(f"{CACHE_KEY_PREFIX}.{quote_id}") if quote is None: # Cache miss: Obtener de la base de datos query_fmt = "SELECT text FROM quotes WHERE id = {}" try: with sqlite3.connect(DB_PATH) as db: cursor = db.cursor() res = cursor.execute( query_fmt.format(quote_id) ).fetchone() if not res: return "No quote found with that id!" quote = res[0] out.append(f"Got '{quote}' FROM DB") except Exception as e: print(e) quote = "" # Agregar a la caché con TTL de 60 segundos if quote: key = f"{CACHE_KEY_PREFIX}.{quote_id}" cache.set(key, quote, ex=60) out.append(f"Added TO CACHE, key '{key}'") else: # Cache hit out.append(f"Got '{quote}' FROM CACHE") return " - ".join(out) if out else ""
Got 'Dark team exactly really wind.' FROM DB - Added TO CACHE, with key 'quote.23'
Enter the ID of the quote: 23
Got 'Dark team exactly really wind.' FROM CACHE
Memoization Pattern
El patrón Memoization es una técnica de optimización crucial que mejora la eficiencia de programas al cachear los resultados de llamadas a funciones costosas. Si una función se llama con los mismos argumentos más de una vez, el resultado cacheado se retorna, eliminando la necesidad de cálculos repetitivos y costosos.
Ejemplos del Mundo Real
Casos de Uso
Comparación de Rendimiento
Implementación con lru_cache
from datetime import timedelta from functools import lru_cache import time # Versión SIN memoization - MUY LENTA def fibonacci_func1(n): if n < 2: return n return fibonacci_func1(n - 1) + fibonacci_func1(n - 2) # Versión CON memoization - ULTRARRÁPIDA @lru_cache(maxsize=None) def fibonacci_func2(n): if n < 2: return n return fibonacci_func2(n - 1) + fibonacci_func2(n - 2) def main(): n = 30 # Test sin caching start_time = time.time() result = fibonacci_func1(n) duration = timedelta(time.time() - start_time) print(f"Fibonacci_func1({n}) = {result}, time: {duration}") # Test con caching start_time = time.time() result = fibonacci_func2(n) duration = timedelta(time.time() - start_time) print(f"Fibonacci_func2({n}) = {result}, time: {duration}") if __name__ == "__main__": main()
Fibonacci_func2(30) = 832040, time: 0:00:02.760315
@lru_cache(maxsize=None) almacena los resultados de las llamadas en memoria. Llamadas repetidas con los mismos argumentos obtienen el resultado directamente del caché sin ejecutar el código de la función.
Lazy Loading Pattern
El patrón Lazy Loading es un enfoque de diseño crítico que difiere la inicialización o carga de recursos hasta el momento en que realmente se necesitan. De esta manera, las aplicaciones logran una utilización de recursos más eficiente, reducen tiempos de carga inicial y mejoran la experiencia del usuario.
Ejemplos del Mundo Real
Casos de Uso
Implementación 1: Lazy Attribute Loading
class LazyLoadedData: def __init__(self): self._data = None # Dato costoso NO cargado aún @property def data(self): # Solo cargar cuando se accede por primera vez if self._data is None: self._data = self.load_data() return self._data def load_data(self): # Simula operación costosa print("Loading expensive data...") return sum(i * i for i in range(100000)) def main(): obj = LazyLoadedData() print("Object created, expensive attribute not loaded yet.") print("Accessing expensive attribute:") print(obj.data) # Aquí se carga print("Accessing again, no reloading occurs:") print(obj.data) # Retorna valor cacheado if __name__ == "__main__": main()
Accessing expensive attribute:
Loading expensive data...
333328333350000
Accessing again, no reloading occurs:
333328333350000
Implementación 2: Lazy Loading con Caching
import time from datetime import timedelta from functools import lru_cache def recursive_factorial(n): """Calcular factorial (costoso para n grande)""" if n == 1: return 1 else: return n * recursive_factorial(n - 1) @lru_cache(maxsize=128) def cached_factorial(n): """Factorial con lazy loading via caching""" return recursive_factorial(n) def main(): n = 20 # Sin caching start_time = time.time() print(f"Recursive factorial of {n}: {recursive_factorial(n)}") duration = timedelta(time.time() - start_time) print(f"Time without caching: {duration}") # Con caching - Primera llamada start_time = time.time() print(f"Cached factorial of {n}: {cached_factorial(n)}") duration = timedelta(time.time() - start_time) print(f"Time with caching: {duration}") # Con caching - Llamada repetida (instantánea) start_time = time.time() print(f"Cached factorial repeated: {cached_factorial(n)}") duration = timedelta(time.time() - start_time) print(f"Second call time: {duration}") if __name__ == "__main__": main()
Time without caching: 0:00:04.840851
Cached factorial of 20: 2432902008176640000
Time with caching: 0:00:00.865173
Cached factorial repeated: 2432902008176640000
Second call time: 0:00:00.350189
lru_cache es inherentemente una herramienta de memoization, pero puede adaptarse para casos de lazy loading donde hay procesos de inicialización costosos que deben ejecutarse solo cuando se requieren.
Resumen de Patrones
01 // Cache-Aside
Gestiona caché efectivamente, asegurando que los datos se obtengan y almacenen de manera que optimice rendimiento y consistencia con fuentes de datos dinámicas.
02 // Memoization
Demuestra el poder de cachear resultados de funciones para acelerar aplicaciones evitando computaciones redundantes en operaciones costosas y repetibles.
03 // Lazy Loading
Retrasa la inicialización de recursos hasta que se necesitan, mejorando tiempos de arranque y reduciendo overhead de memoria.