Context managers en Python: el protocolo with, __enter__ y __exit__

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.

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP