Un gestor de contexto es cualquier objeto que implementa __enter__ y __exit__, el mecanismo que está detrás de with open(...) as f. Escribir uno desde cero requiere una clase completa. El módulo contextlib proporciona herramientas para crear gestores de contexto de forma mucho más sencilla.
@contextmanager: gestor de contexto con un generador
El decorador @contextmanager convierte un generador en un gestor de contexto. El código antes del yield actúa como __enter__; el código después, como __exit__. El valor del yield es lo que recibe la cláusula as.
from contextlib import contextmanager
import time
@contextmanager
def timer(etiqueta: str):
inicio = time.perf_counter()
try:
yield # aquí se ejecuta el bloque with
finally:
fin = time.perf_counter()
print(f"[{etiqueta}] {fin - inicio:.3f}s")
with timer("ordenar lista"):
datos = list(range(100_000, 0, -1))
datos.sort()
# [ordenar lista] 0.012s
from contextlib import contextmanager
import sqlite3
@contextmanager
def transaccion(ruta_db: str):
"""Gestiona una transacción: commit si todo va bien, rollback si hay excepción."""
conn = sqlite3.connect(ruta_db)
try:
yield conn
conn.commit()
print("Commit realizado")
except Exception as e:
conn.rollback()
print(f"Rollback por: {e}")
raise
finally:
conn.close()
with transaccion("mi_base.db") as conn:
conn.execute("INSERT INTO usuarios (nombre) VALUES (?)", ("Ana",))
contextlib.suppress ignorar excepciones específicas
suppress(*excepciones) suprime las excepciones listadas si se lanzan dentro del bloque. Reemplaza bloques try/except: pass.
from contextlib import suppress
import os
# Sin suppress:
try:
os.remove("fichero_que_puede_no_existir.txt")
except FileNotFoundError:
pass
# Con suppress:
with suppress(FileNotFoundError):
os.remove("fichero_que_puede_no_existir.txt")
# También con múltiples tipos de excepción:
with suppress(FileNotFoundError, PermissionError):
os.remove("/ruta/protegida.txt")
contextlib.nullcontext gestor de contexto vacío
nullcontext es útil cuando necesitas un gestor de contexto de forma condicional pero no quieres añadir lógica extra.
from contextlib import nullcontext
import threading
def procesar(datos: list, usar_lock: bool = False):
lock = threading.Lock() if usar_lock else nullcontext()
with lock:
# El código funciona igual con o sin lock
resultado = sum(datos)
return resultado
print(procesar([1, 2, 3])) # sin lock
print(procesar([1, 2, 3], usar_lock=True)) # con lock
from contextlib import nullcontext
def abrir_fichero(path: str | None):
# Si no hay path, usa nullcontext; si hay, abre el fichero
ctx = open(path) if path else nullcontext()
with ctx as f:
if f:
contenido = f.read()
else:
contenido = "sin fichero"
return contenido
ExitStack gestionar un número variable de contextos
ExitStack te permite apilar dinámicamente cualquier número de gestores de contexto, algo que no puedes hacer con with estático.
from contextlib import ExitStack
rutas = ["archivo1.txt", "archivo2.txt", "archivo3.txt"]
with ExitStack() as stack:
ficheros = [stack.enter_context(open(r)) for r in rutas]
for f in ficheros:
print(f.readline().strip())
# Todos los ficheros se cierran al salir del bloque
from contextlib import ExitStack, contextmanager
@contextmanager
def recurso(nombre: str):
print(f"Abriendo {nombre}")
try:
yield nombre
finally:
print(f"Cerrando {nombre}")
# Gestores de contexto definidos dinámicamente en tiempo de ejecución
nombres = ["db", "cache", "log"]
with ExitStack() as stack:
recursos = [stack.enter_context(recurso(n)) for n in nombres]
print(f"Trabajando con: {recursos}")
# Abriendo db / Abriendo cache / Abriendo log
# Trabajando con: ['db', 'cache', 'log']
# Cerrando log / Cerrando cache / Cerrando db (orden inverso)
@asynccontextmanager para código async
from contextlib import asynccontextmanager
import asyncio
@asynccontextmanager
async def conexion_async(host: str):
print(f"Conectando a {host}...")
await asyncio.sleep(0.1) # simula conexión
try:
yield {"host": host, "activa": True}
finally:
print(f"Cerrando conexión a {host}")
await asyncio.sleep(0.05)
async def main():
async with conexion_async("db.ejemplo.com") as conn:
print(f"Usando conexión: {conn}")
asyncio.run(main())
La regla para elegir herramienta: @contextmanager para la mayoría de los casos; suppress en lugar de try/except: pass; nullcontext para contextos opcionales; ExitStack cuando el número de recursos no se conoce hasta tiempo de ejecución. Evita gestores de contexto que silencien excepciones inesperadas: suppress solo debe usarse con los tipos exactos de error que esperas.
