Skip to content

📙 Clase 28 — Context managers: tu propio with

Fase 5 · Python intermedio / "pro" ⬅️ Volver al índice de clases

🎯 Qué aprendí

  • Qué hace with por dentro: el protocolo __enter__ / __exit__.
  • Crear context managers propios (clase o @contextmanager).
  • La joya para la Fase 6: with con: = transacción automática en SQLite.

📖 PARTE TEÓRICA

🚪 1. El protocolo detrás de with

Usas with open(...) desde la Clase 19. Su magia son dos dunders (Clase 17):

with OBJETO as x:      ①  x = OBJETO.__enter__()        (al ENTRAR)
    ...bloque...
                       ②  OBJETO.__exit__(...)          (al SALIR — SIEMPRE,
                                                          incluso con excepción)

Es el try/finally de la Clase 12, empaquetado y reutilizable: preparar → usar → limpiar garantizado.

🛠️ 2. Crear uno con clase

python
import time

class Cronometro:
    def __enter__(self):
        self.inicio = time.perf_counter()
        return self                        # esto es lo que recibe el 'as'

    def __exit__(self, tipo, valor, tb):   # info de la excepción (si hubo)
        self.ms = (time.perf_counter() - self.inicio) * 1000
        print(f"bloque tardó {self.ms:.1f} ms")
        return False                       # False = NO silenciar excepciones

with Cronometro():
    sum(range(100_000))
# bloque tardó 0.8 ms

⚠️ Si __exit__ devuelve True, la excepción del bloque se traga. Casi siempre quieres False (o no devolver nada): limpiar sí, esconder errores no.

🍬 3. La forma corta: @contextmanager

Un generador (Clase 25) + un decorador (Clase 26) = context manager en 6 líneas. Toda la Fase 5 en una función:

python
from contextlib import contextmanager

@contextmanager
def abriendo_seccion(nombre):
    print(f">> entrando a {nombre}")      # ANTES del bloque (el __enter__)
    try:
        yield nombre                       # AQUÍ corre el bloque; yield = el 'as'
    finally:
        print(f"<< saliendo de {nombre}")  # DESPUÉS, pase lo que pase (el __exit__)

with abriendo_seccion("config") as s:
    print(f"trabajando en {s}")
# >> entrando a config
# trabajando en config
# << saliendo de config

🧪 Tip de entrevista: "¿Qué hace with y cómo creas el tuyo?" → Ejecuta __enter__/__exit__ garantizando la limpieza (como try/finally). Se crea con una clase que implemente ambos, o con @contextlib.contextmanager sobre un generador de un yield.


🖥️ EN TU APP DE ESCRITORIO

Un context manager elegante para operaciones largas — cursor de espera que siempre vuelve a la normalidad:

python
from contextlib import contextmanager

class App(ctk.CTk):
    @contextmanager
    def ocupado(self, mensaje="Trabajando…"):
        self.status.configure(text=f"⏳ {mensaje}")
        self.configure(cursor="watch")         # cursor de espera
        self.update_idletasks()                 # refrescar YA la pantalla
        try:
            yield
        finally:                                # aunque la operación explote…
            self.configure(cursor="")           # …el cursor SIEMPRE se restaura
            self.status.configure(text="")

    def _importar(self):
        with self.ocupado("Importando CSV…"):
            self._leer_csv_grande()             # si lanza excepción, igual se limpia

Sin esto, una excepción a mitad de la importación te deja el cursor de espera para siempre — el clásico bug de apps amateur.


🗄️ CON BASE DE DATOS (la conexión directa a la Fase 6)

sqlite3 trae dos regalos con with que usarás en cada operación de escritura:

python
import sqlite3
con = sqlite3.connect("app.db")

# ① with con:  →  TRANSACCIÓN automática
with con:                                     # al salir bien → COMMIT solo
    con.execute("INSERT INTO t VALUES (1)")

try:
    with con:
        con.execute("INSERT INTO t VALUES (2)")
        raise ValueError("algo falló")        # → ROLLBACK automático: el 2 NO queda
except ValueError:
    pass
# verificado: tras el rollback la tabla solo tiene el 1 ✔

🗄️ Regla de la Fase 6: toda escritura (INSERT/UPDATE/DELETE) va dentro de with con:. O se guarda todo o nada — jamás datos a medias. (Ojo al matiz: with con: NO cierra la conexión, solo maneja la transacción; cerrar sigue siendo con.close() o contextlib.closing.)


🏋️ EJERCICIOS CON SOLUCIÓN

Ejercicio 1 — Marcador de bloque

Crea con @contextmanager un marcado() que imprima "[inicio]" antes y "[fin]" después del bloque.

Ver solución
python
from contextlib import contextmanager

@contextmanager
def marcado():
    print("[inicio]")
    try:
        yield
    finally:
        print("[fin]")

with marcado():
    print("hola")
# [inicio]
# hola
# [fin]

Ejercicio 2 — Limpieza garantizada

Demuestra que el "[fin]" de tu marcado() se imprime aunque el bloque lance una excepción.

Ver solución
python
try:
    with marcado():
        raise ValueError("boom")
except ValueError:
    pass
# [inicio]
# [fin]      ← se imprimió igual: el finally del generador SIEMPRE corre

Ejercicio 3 — Con clase

Reescribe marcado como clase con __enter__/__exit__.

Ver solución
python
class Marcado:
    def __enter__(self):
        print("[inicio]")
        return self
    def __exit__(self, tipo, valor, tb):
        print("[fin]")
        return False       # no silenciar excepciones

with Marcado():
    print("hola")

❓ Preguntas y respuestas (autoevaluación)

1. ¿Qué dos métodos implementa un context manager?

__enter__ (al entrar; su retorno va al as) y __exit__ (al salir, siempre).

2. ¿Qué significa que __exit__ devuelva True?

Que la excepción del bloque queda silenciada. Normalmente devuelves False.

3. En la forma @contextmanager, ¿qué separa el "antes" del "después"?

El único yield: lo previo es el enter, lo del finally es el exit.

4. ¿Qué hace with con: sobre una conexión sqlite3 (y qué NO hace)?

Maneja la transacción: commit al salir bien, rollback si hubo excepción. No cierra la conexión.


📎 Apuntes relacionados

➡️ Siguiente

¡Fase 5 completa! 🎉 Llegó tu especialidad — Fase 6: Clase 29 · Arquitectura de una app.