Gestores de Contexto como Patrón
Los gestores de contexto garantizan la configuración y la limpieza de un bloque de código. La declaración with llama a __enter__/__exit__ (o @contextmanager yield) para que los recursos se liberen incluso cuando ocurren excepciones: los archivos se cierran, los bloqueos se liberan y las transacciones se revierten.
Receta
Tarjeta de referencia rápida - lista para copiar y pegar.
from contextlib import contextmanager
@contextmanager
def temporary_setting(obj, attr, value):
old = getattr(obj, attr)
setattr(obj, attr, value)
try:
yield obj
finally:
setattr(obj, attr, old)
with temporary_setting(config, "debug", True):
run_checks()Cuándo usar esto:
- Pares de adquisición/liberación - archivos, sockets, bloqueos, conexiones de base de datos
- Estado temporal - variables de entorno, niveles de registro, flags de características para un bloque
- Transacciones - confirmar al éxito, revertir ante excepción
- Medición de tiempo y perfilado -
enterinicia el temporizador,exitregistra la duración - Suprimir errores esperados -
contextlib.suppress(FileNotFoundError)
Ejemplo de Trabajo
Un gestor basado en clases para transacciones de base de datos más un @contextmanager para la medición de tiempo.
import time
from contextlib import contextmanager
from dataclasses import dataclass, field
from typing import Iterator
@dataclass
class FakeConnection:
committed: bool = False
rolled_back: bool = False
def commit(self) -> None:
self.committed = True
def rollback(self) -> None:
self.rolled_back = True
class Transaction:
def __init__(self, conn: FakeConnection) -> None:
self._conn = conn
def __enter__(self) -> FakeConnection:
return self._conn
def __exit__(self, exc_type, exc, tb) -> bool:
if exc_type is None:
self._conn.commit()
else:
self._conn.rollback()
return False # no suprimir excepciones
@contextmanager
def timer(label: str) -> Iterator[None]:
start = time.perf_counter()
try:
yield
finally:
elapsed = time.perf_counter() - start
print(f"{label}: {elapsed:.3f}s")
conn = FakeConnection()
with timer("ruta feliz"):
with Transaction(conn) as db:
print("escribir filas", db)
print("confirmado:", conn.committed)
conn2 = FakeConnection()
try:
with Transaction(conn2):
raise ValueError("boom")
except ValueError:
pass
print("revertido:", conn2.rolled_back)Lo que esto demuestra:
- Protocolo de clase:
__enter__devuelve el recurso,__exit__limpia - Devolver
Falsedesde__exit__propaga las excepciones después de la reversión @contextmanagerdivide la configuración (antes deyield) y la limpieza (finally)finallyse ejecuta tanto si el bloque se sale normalmente como si es por una excepción
Inmersión Profunda
Cómo Funciona
with expr as var:evalúaexpr, llama a__enter__, vinculavar, ejecuta la suite- Al salir,
__exit__(exc_type, exc_val, tb)siempre se ejecuta - Si
__exit__devuelve un valor verdadero, la excepción se suprime @contextmanagerenvuelve un generador: el código antes deyieldes la entrada, el de después es la salidacontextlib.ExitStackcompone múltiples gestores sin anidamiento profundo
Tipos Comunes de Gestores
| Patrón | Entrada | Salida |
|---|---|---|
| E/S de Archivo | abrir manejador | cerrar manejador |
| Bloqueo | adquirir | liberar |
| Transacción | iniciar | confirmar o revertir |
| Directorio temporal | crear ruta | eliminar árbol |
Notas de Python
from contextlib import ExitStack, suppress
with ExitStack() as stack:
f1 = stack.enter_context(open("a.txt"))
f2 = stack.enter_context(open("b.txt"))
# ambos se cierran al salir
with suppress(FileNotFoundError):
open("optional.cfg").read()Errores Comunes
- Olvidar
finallyen generadores - la limpieza debe estar enfinallyalrededor deyield, no solo después si tiene éxito. Solución: siempre envolver el código posterior ayieldentry/finally. - Suprimir excepciones accidentalmente - que
__exit__devuelvaTrueoculta errores. Solución: devolverFalsea menos que la supresión sea intencional (documentarlo). - Bloqueos reentrantes - no todos los gestores son reentrantes; anidar el mismo bloqueo incorrectamente causa interbloqueo. Solución: usar
threading.RLocko documentar la no reentrancia. - Generar solo una vez -
@contextmanagerdebe generar exactamente un valor. Solución: no generar en un bucle. - Fuga de recursos ante fallo de
__enter__- si la configuración tiene éxito parcialmente y luego falla, la salida puede no ejecutarse. Solución: usarExitStacko mantener la configuración mínima en__enter__.
Alternativas
| Alternativa | Usar Cuando | No Usar Cuando |
|---|---|---|
try/finally manual | Ocasional, no se necesita reutilización | El patrón se repite en la base de código |
Manejadores atexit | Limpieza al final del proceso | Limpieza por solicitud o por ámbito |
weakref.finalize | Limpieza de objetos impulsada por GC | Se requiere tiempo determinista |
| Decoradores | Envolver funciones completas | Bloques arbitrarios en línea necesitan with |
Preguntas Frecuentes
¿Cuál es la diferencia entre un gestor de clase y @contextmanager?
Las clases implementan __enter__/__exit__. @contextmanager convierte un generador con un solo yield en el mismo protocolo - menos código repetitivo para casos simples.
¿Puede __exit__ suprimir excepciones?
Sí, devolviendo True. Usar con moderación - los llamadores pierden visibilidad de los fallos a menos que ese sea el contrato (suppress).
¿Funciona with con código asíncrono?
Usar async with y __aenter__/__aexit__, o @asynccontextmanager. with síncrono no puede esperar.
¿A qué se vincula as var?
Al valor de retorno de __enter__ (o al valor generado por @contextmanager). Muchos gestores devuelven self o el recurso subyacente.
¿Cómo compongo muchos gestores?
contextlib.ExitStack entra en cada gestor y se desenrolla en orden inverso al salir.
¿Es open() un gestor de contexto?
Sí. with open(path) as f cierra el archivo incluso si la lectura genera un error.
¿Puedo usar gestores de contexto en dataclass __post_init__?
Es preferible que los llamadores usen with alrededor del ámbito de construcción. Los objetos de larga duración deben exponer .close() o usar finalizadores de weakref.
¿Qué pasa con contextlib.closing?
Envuelve objetos con un método .close() en un gestor de contexto cuando carecen de soporte nativo.
¿Deberían las transacciones usar siempre gestores de contexto?
Sí, para el código de aplicación - el emparejamiento commit/rollback es propenso a errores sin un __exit__ garantizado.
¿Cómo pruebo los gestores de contexto?
Asegurar el estado del recurso antes, dentro y después de with. Provocar excepciones dentro del bloque para verificar que la limpieza aún se ejecuta.
Relacionado
- EAFP vs LBYL - excepciones dentro de bloques gestionados
- Patrones Factory y Builder - construcción de recursos
- Inyección de Dependencias en Python - inyección de conexiones
- Conceptos Básicos de Patrones Pythonicos - resumen de la sección
Versiones de Stack: Esta página fue escrita para Python 3.14.0 (estable 3.14, mantenimiento 3.13), FastAPI 0.115+, Django 5.2, Flask 3.1, Pydantic 2, PyTorch 2.6+, pandas 2.2+, Polars 1.x, ruff 0.9+, y uv 0.6+.