Apariencia
📙 Clase 28 — Context managers: tu propio with
Fase 5 · Python intermedio / "pro" ⬅️ Volver al índice de clases
🎯 Qué aprendí
- Qué hace
withpor 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__devuelveTrue, la excepción del bloque se traga. Casi siempre quieresFalse(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
withy 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.contextmanagersobre un generador de unyield.
🖥️ 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 limpiaSin 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 dewith 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 siendocon.close()ocontextlib.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 correEjercicio 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 alas) 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 delfinallyes 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
- El
with open()que imitamos → Clase 19 - Dunders → Clase 17 · Generadores → Clase 25 · Decoradores → Clase 26
➡️ Siguiente
¡Fase 5 completa! 🎉 Llegó tu especialidad — Fase 6: Clase 29 · Arquitectura de una app.