El bloque with no es solo para abrir ficheros. Los context managers garantizan que un recurso se libera correctamente al salir del bloque, haya error o no. Implementarlos en tus propias clases o con contextlib.contextmanager añade esta garantía a cualquier operación que necesite setup y teardown.
El protocolo: __enter__ y __exit__
class ConexionBD:
def __init__(self, dsn):
self.dsn = dsn
self.conn = None
def __enter__(self):
print(f"Abriendo conexión a {self.dsn}")
self.conn = {"dsn": self.dsn, "estado": "conectado"} # simulación
return self.conn # lo que recibe la variable 'as'
def __exit__(self, tipo_exc, valor_exc, traceback):
print("Cerrando conexión")
self.conn["estado"] = "cerrado"
# Devuelve False (o None) para NO suprimir excepciones
# Devuelve True para suprimir la excepción
return False
with ConexionBD("postgresql://localhost/midb") as conn:
print(f"Usando conexión: {conn['estado']}")
# Si aquí lanzamos una excepción, __exit__ se llama igual
Suprimir excepciones seleccionadas con __exit__
class IgnorarErroresLectura:
def __enter__(self):
return self
def __exit__(self, tipo, valor, tb):
if tipo is FileNotFoundError:
print(f"Fichero no encontrado, ignorando: {valor}")
return True # suprime FileNotFoundError
return False # cualquier otro error sigue propagándose
with IgnorarErroresLectura():
with open("fichero_inexistente.txt") as f:
datos = f.read()
print("Continuamos después del bloque") # se imprime: el error fue suprimido
contextlib.contextmanager: la forma rápida
@contextmanager convierte un generador con un único yield en un context manager, eliminando la necesidad de escribir una clase completa:
from contextlib import contextmanager
@contextmanager
def cronometro(etiqueta=""):
import time
inicio = time.perf_counter()
try:
yield # aquí se ejecuta el bloque 'with'
finally:
duracion = time.perf_counter() - inicio
print(f"{etiqueta}: {duracion:.4f}s")
with cronometro("Cálculo"):
resultado = sum(range(1_000_000))
# Cálculo: 0.0412s
Ejemplo real: transacción de base de datos
from contextlib import contextmanager
@contextmanager
def transaccion(conexion):
"""Context manager que hace commit o rollback automáticamente."""
try:
yield conexion
conexion.commit()
print("Transacción completada (commit)")
except Exception as e:
conexion.rollback()
print(f"Error rollback: {e}")
raise
# Uso:
# with transaccion(conn) as c:
# c.execute("INSERT INTO pedidos VALUES (...)")
# c.execute("UPDATE stock SET cantidad = cantidad - 1 WHERE ...")
Ejemplo real: directorio temporal
from contextlib import contextmanager
import tempfile
import shutil
import os
@contextmanager
def directorio_temporal():
"""Crea un directorio temporal y lo elimina al salir."""
tmp = tempfile.mkdtemp()
try:
yield tmp
finally:
shutil.rmtree(tmp, ignore_errors=True)
print(f"Directorio temporal {tmp} eliminado")
with directorio_temporal() as d:
ruta = os.path.join(d, "datos.txt")
with open(ruta, "w") as f:
f.write("contenido temporal")
print(f"Fichero creado en {ruta}")
# Directorio temporal /tmp/tmpXXXXXX eliminado
contextlib.suppress y contextlib.ExitStack
from contextlib import suppress, ExitStack
# suppress: ignora excepciones específicas en una línea
with suppress(FileNotFoundError):
os.remove("fichero_que_puede_no_existir.tmp")
# ExitStack: gestionar un número dinámico de context managers
with ExitStack() as stack:
ficheros = [stack.enter_context(open(f)) for f in ["a.txt", "b.txt"]]
# todos se cierran al salir
Usa @contextmanager para la mayoría de los casos: es más conciso que una clase y suficientemente expresivo. Solo implementa __enter__/__exit__ directamente cuando el context manager forme parte de una clase con más responsabilidades.
