Esta es la lección de saldar la deuda más antigua del curso. Desde el módulo 6 escribes with ruta.open() as f: aceptando dos promesas: "el archivo se cierra solo, pase lo que pase" y "algún día veremos cómo funciona por dentro". El módulo 7 añadió la pista clave — el try/finally garantiza la limpieza incluso con excepciones — y la lección anterior te enseñó un with dentro de un generador. Hoy se cierra el círculo: el protocolo __enter__/__exit__ que hay detrás de cada with, y con él construiremos administradores de contexto propios para Papyrus, incluida una TransaccionCatalogo que deja el catálogo a salvo si una venta se rompe a medias.
Contenido
- Qué hace realmente el
with - El protocolo:
__enter__y__exit__ - Reimplementar (conceptualmente) el
withde archivos - Un administrador propio como clase:
TransaccionCatalogo __exit__y las excepciones: propagar o suprimircontextlib.@contextmanager: la versión generador- Otros usos: cronometrar un bloque, cambiar de directorio
- Clase vs
@contextmanager: tabla de decisión
Qué hace realmente el with
Empecemos por lo que el with no es: no es un bloque mágico "para archivos". Es azúcar sintáctico sobre un patrón try/finally que ya sabrías escribir a mano con lo aprendido en 07-02:
# Lo que escribes:
with (BASE / "datos" / "catalogo.json").open(encoding="utf-8") as f:
datos = f.read()
# Lo que Python ejecuta (equivalente esencial):
f = (BASE / "datos" / "catalogo.json").open(encoding="utf-8")
f_ctx = f.__enter__() # preparar el contexto; su retorno va al "as"
try:
datos = f_ctx.read()
finally:
f.__exit__(None, None, None) # limpiar SIEMPRE: con éxito o con excepciónCualquier objeto que implemente esos dos métodos dunder — y ya sabes de dunders por el módulo 5 — puede usarse con with. Se le llama administrador de contexto (context manager): administra la entrada y la salida de un contexto, garantizando la salida.
El protocolo: __enter__ y __exit__
| Método | Cuándo se llama | Qué hace | Qué devuelve |
|---|---|---|---|
__enter__(self) |
Al entrar en el with |
Adquirir/preparar el recurso | Lo que quieras ligar al as (a menudo self) |
__exit__(self, exc_type, exc_val, exc_tb) |
Al salir, siempre | Liberar/limpiar | True → suprime la excepción; False/None → la propaga |
Los tres argumentos de __exit__ describen qué pasó dentro del bloque:
- Si el bloque terminó bien: los tres son
None. - Si el bloque lanzó:
exc_typees la clase de la excepción (p. ej.StockInsuficienteError),exc_valla instancia (con sus atributostitulo,solicitado,disponiblede 07-04) yexc_tbel traceback.
Es decir: __exit__ es un finally con superpoderes — no solo se ejecuta siempre, sino que además sabe si hubo excepción y cuál.
Reimplementar el with de archivos
Para fijar el concepto, escribamos nuestro propio "open" conceptual (el de verdad está en C, pero el protocolo es idéntico):
class Archivo:
"""Reimplementación didáctica del context manager de open()."""
def __init__(self, ruta, modo="r", encoding="utf-8"):
self.ruta = ruta
self.modo = modo
self.encoding = encoding
self.f = None
def __enter__(self):
print(f"[enter] abriendo {self.ruta}")
self.f = open(self.ruta, self.modo, encoding=self.encoding)
return self.f # esto es lo que recibe el "as f"
def __exit__(self, exc_type, exc_val, exc_tb):
print(f"[exit] cerrando {self.ruta} (excepción: {exc_type})")
self.f.close() # pase lo que pase
return False # no suprimimos nada: que se propague
with Archivo(BASE / "datos" / "registro_altas.txt") as f:
print(f.readline())Si dentro del bloque algo lanza, verás [exit] cerrando ... (excepción: <class '...'>) antes de que la traza llegue a tu pantalla: la limpieza ocurrió, y la excepción siguió su camino hacia las anillas de 07-04. Exactamente las dos promesas del with, ahora sin misterio.
TransaccionCatalogo: el caso real de Papyrus
Ahora un administrador que resuelve un problema de verdad. vender() descuenta stock y luego se registra la venta y se guarda el catálogo. ¿Qué pasa si Ana vende un lote de tres títulos a Marta y el segundo lanza StockInsuficienteError? El primero ya se descontó: el catálogo queda a medias, en un estado que no corresponde a ninguna venta real. Queremos semántica de transacción: o todo, o nada.
import copy
import logging
logger = logging.getLogger(__name__)
class TransaccionCatalogo:
"""Si el bloque falla, restaura el catálogo a su estado inicial y relanza."""
def __init__(self, catalogo: dict):
self.catalogo = catalogo
self.copia = None
def __enter__(self):
self.copia = copy.deepcopy(self.catalogo) # foto del estado inicial
return self.catalogo # se trabaja sobre el original
def __exit__(self, exc_type, exc_val, exc_tb):
if exc_type is not None: # algo se rompió dentro
self.catalogo.clear()
self.catalogo.update(self.copia) # restaurar la foto
logger.warning("transacción revertida por %s: %s",
exc_type.__name__, exc_val)
return False # False → la excepción se propagadeepcopy (primo profundo del copy que rozamos en M4) fotografía catálogo y libros; restaurar mutando con clear()/update() — en vez de reasignar — mantiene válida cualquier otra referencia al mismo diccionario. Su uso en el mostrador:
from almacen import vender
from errores import ErrorPapyrus
lote = [("Hamlet", 2), ("La Odisea", 9), ("Fausto", 1)] # La Odisea: solo hay 4
try:
with TransaccionCatalogo(catalogo) as cat:
for titulo, unidades in lote:
vender(cat, titulo, unidades)
except ErrorPapyrus as e:
print(f"Venta cancelada, el catálogo está intacto: {e}")Secuencia: se venden 2 Hamlet (stock 6 → 4)... y La Odisea con 9 unidades pedidas y 4 disponibles lanza StockInsuficienteError. __exit__ la ve llegar, restaura la foto — Hamlet vuelve a 6 — deja constancia a WARNING en papyrus.log, y devuelve False: la excepción sigue subiendo hasta el except ErrorPapyrus del mostrador, que informa con la calma de 07-04. Nadie perdió datos; nadie silenció nada.
Propagar o suprimir: el retorno de __exit__
El valor devuelto por __exit__ es un interruptor delicado:
False(oNone, que es lo que devuelve un método sinreturn): la excepción se propaga. Es lo correcto en el 95 % de los casos — el administrador limpia, no decide.True: la excepción se suprime, como si el bloque hubiera ido bien. Solo tiene sentido cuando suprimir es el servicio que ofreces (p. ej.contextlib.suppress(FileNotFoundError), que la biblioteca estándar ya trae hecho).
Un return True descuidado es el except: pass de 07-04 disfrazado de elegancia. Si dudas, False.
contextlib.@contextmanager: la versión generador
Escribir una clase entera para "haz algo antes, haz algo después" puede ser mucha ceremonia. contextlib ofrece un atajo que reúne las dos lecciones anteriores — un decorador aplicado a un generador:
from contextlib import contextmanager
@contextmanager
def transaccion_catalogo(catalogo: dict):
copia = copy.deepcopy(catalogo) # ── esto hace de __enter__
try:
yield catalogo # ── aquí se ejecuta el bloque del with
except Exception:
catalogo.clear()
catalogo.update(copia) # ── esto hace de __exit__ con excepción
raise # relanzar = "return False" de la claseLa correspondencia exacta:
- Todo lo anterior al
yieldes el__enter__. - El valor del
yieldes lo que recibe elas. - El generador queda pausado en el
yield(tal cual lo viste en 08-03) mientras se ejecuta el cuerpo delwith. - Si el bloque lanza, la excepción aparece en el punto del
yield— por eso se envuelve entry/except(otry/finallysi solo hay que limpiar). - Reanudar y terminar sin más equivale a propagar normalidad;
raisepropaga la excepción; atraparla sin relanzar equivale alreturn True.
El mismo comportamiento que la clase, en un tercio de líneas.
Otros usos: no solo recursos
Cualquier patrón "preparar / deshacer" encaja. Dos ejemplos útiles en Papyrus:
import time
from contextlib import contextmanager
@contextmanager
def cronometro(etiqueta: str):
"""El primo de bloque del @cronometrar de 08-02: mide un TROZO de código."""
inicio = time.perf_counter()
try:
yield
finally:
logger.info("%s: %.3f s", etiqueta, time.perf_counter() - inicio)
with cronometro("cierre + informe"):
total = cierre_de_caja(BASE / "datos" / "ventas.csv")
generar_informe(total)import os
from pathlib import Path
@contextmanager
def en_directorio(ruta: Path):
"""Cambia el cwd temporalmente y SIEMPRE vuelve (útil en informes/)."""
anterior = Path.cwd()
os.chdir(ruta)
try:
yield ruta
finally:
os.chdir(anterior) # aunque generar el informe exploteFíjate en que cronometro hace con un bloque lo que @cronometrar hacía con una función: decoradores y context managers son las dos caras de "envolver código".
Clase vs @contextmanager
| Criterio | Clase (__enter__/__exit__) |
@contextmanager |
|---|---|---|
| Líneas de código | Más (ceremonia de clase) | Menos (un generador) |
| Estado complejo o métodos extra | Natural (atributos, métodos) | Incómodo |
| Reutilizable/configurable con herencia | Sí | No aplica |
| Distinguir tipos de excepción en la salida | exc_type explícito |
except TipoX: dentro del generador |
| Legibilidad para casos simples | Correcta | Excelente |
| Ejemplo idóneo | TransaccionCatalogo con opciones |
cronometro, en_directorio |
Regla práctica: empieza con @contextmanager; pásate a la clase cuando necesites estado, configuración o una jerarquía.
Errores Comunes y Consejos
- Olvidar el
tryalrededor delyielden un@contextmanager: si el bloque lanza, tu código de limpieza no se ejecuta — el generador muere en elyield. Eltry/finally(otry/except+raise) no es opcional: es el equivalente del__exit__. return True(o atrapar sin relanzar) por accidente: suprime excepciones que el mostrador esperaba. Síntoma típico: "la venta falló pero no saltó ningún error". Revisa el retorno de__exit__antes que nada.- Hacer trabajo pesado en
__init__en vez de__enter__:TransaccionCatalogo(catalogo)sinwithno debería fotografiar nada todavía.__init__guarda parámetros;__enter__adquiere. - Copiar superficialmente cuando hay objetos anidados:
dict(catalogo)copia el diccionario pero no losLibro; la reversión dejaría stocks corruptos. Para transacciones,copy.deepcopy. - Consejo: si escribes
try/finallyalrededor de un recurso más de una vez, es la señal de que ahí vive un context manager con nombre propio. - Consejo: se pueden encadenar en un solo
with:with transaccion_catalogo(cat) as c, cronometro("venta"):— se salen en orden inverso al que se entró.
Ejercicios
- Escribe como clase el administrador
RegistroBloque(nombre)que al entrar escriba en el logINICIO <nombre>y al salirFIN <nombre> (ok)oFIN <nombre> (error: <ExcTipo>)según haya habido excepción, propagándola siempre. Pruébalo envolviendo una venta que lanceStockInsuficienteError. - Reescribe
RegistroBloquecon@contextmanager(llámaloregistro_bloque). ¿Qué construcción sustituye a la comprobaciónif exc_type is not None? - Escribe
copia_de_seguridad(ruta)con@contextmanager: antes del bloque copia el archivo acopias/<nombre>.bak(usashutil.copy2, de M6); si el bloque falla, restaura el.baksobre el original y relanza; si va bien, no hace nada más. Úsalo alrededor deguardar_catalogo.
Soluciones
-
class RegistroBloque: def __init__(self, nombre: str): self.nombre = nombre def __enter__(self): logger.info("INICIO %s", self.nombre) return self def __exit__(self, exc_type, exc_val, exc_tb): if exc_type is None: logger.info("FIN %s (ok)", self.nombre) else: logger.error("FIN %s (error: %s)", self.nombre, exc_type.__name__) return False # propagar siempre with RegistroBloque("venta lote Marta"): vender(catalogo, "La Odisea", 9) # lanza StockInsuficienteErrorEn
papyrus.logquedaINICIO+FIN ... (error: StockInsuficienteError), y la excepción sigue hacia el mostrador. -
@contextmanager def registro_bloque(nombre: str): logger.info("INICIO %s", nombre) try: yield logger.info("FIN %s (ok)", nombre) except Exception as e: logger.error("FIN %s (error: %s)", nombre, type(e).__name__) raiseLa comprobación
if exc_type is not Nonese convierte en la estructuratry/except: el camino sin excepción sigue tras elyield; el camino con excepción entra por elexcept— y elraisefinal preserva la propagación. -
import shutil from contextlib import contextmanager @contextmanager def copia_de_seguridad(ruta: Path): respaldo = BASE / "copias" / (ruta.name + ".bak") shutil.copy2(ruta, respaldo) try: yield ruta except Exception: shutil.copy2(respaldo, ruta) # restaurar el original raise with copia_de_seguridad(BASE / "datos" / "catalogo.json") as ruta: guardar_catalogo(catalogo, ruta)Es la
TransaccionCatalogollevada al disco: la misma idea (foto → intentar → restaurar si falla → relanzar) aplicada a un archivo en vez de a un diccionario.
Conclusión
Promesa saldada: el with era un protocolo de dos dunders — __enter__ prepara, __exit__ limpia siempre y decide con su retorno si la excepción se propaga (casi siempre False) — montado sobre el mismo try/finally de 07-02. Papyrus ganó TransaccionCatalogo, que garantiza ventas de todo-o-nada, y las versiones @contextmanager demostraron que decoradores (08-02) y generadores (08-03) no eran temas sueltos: aquí trabajan juntos. Con esto, el código de Papyrus es expresivo y eficiente... pero sigue haciendo una cosa cada vez: si consultar el precio de un libro a una distribuidora tarda dos segundos, consultar a tres tarda seis, con el programa parado esperando. Las dos últimas lecciones del módulo atacan esa espera: primero con hilos y procesos — y una conversación honesta sobre el famoso GIL — y después con asyncio.
Curso de Programación en Python
Módulo 1: Introducción a Python
- Introducción a Python
- Configuración del Entorno de Desarrollo
- Sintaxis de Python y Tipos de Datos Básicos
- Variables y Constantes
- Entrada y Salida Básica
- Entornos Virtuales y Gestión de Paquetes
Módulo 2: Estructuras de Control
- Sentencias Condicionales
- Bucles: for y while
- Herramientas de Control de Flujo
- Comprensiones de Listas
Módulo 3: Funciones y Módulos
- Definición de Funciones
- Argumentos de Función
- Funciones Lambda
- Módulos y Paquetes
- Visión General de la Biblioteca Estándar
Módulo 4: Estructuras de Datos
Módulo 5: Programación Orientada a Objetos
Módulo 6: Manejo de Archivos
- Lectura y Escritura de Archivos
- Trabajo con Archivos CSV
- Manejo de Datos JSON
- Operaciones con Archivos y Directorios
Módulo 7: Manejo de Errores y Excepciones
- Introducción a las Excepciones
- Manejo de Excepciones
- Lanzamiento de Excepciones
- Excepciones Personalizadas
- Buenas Prácticas y Registro de Errores con logging
Módulo 8: Temas Avanzados
- Anotaciones de Tipos (type hints)
- Decoradores
- Generadores
- Administradores de Contexto
- Concurrencia: Hilos y Procesos
- Asyncio para Programación Asíncrona
Módulo 9: Pruebas y Depuración
- Introducción a las Pruebas
- Pruebas Unitarias con unittest
- Pruebas con pytest
- Desarrollo Guiado por Pruebas
- Técnicas de Depuración
- Uso de pdb para Depuración
Módulo 10: Desarrollo Web con Python
- Introducción al Desarrollo Web
- Fundamentos del Framework Flask
- Construcción de APIs REST con Flask
- Introducción a Django
- Construcción de Aplicaciones Web con Django
Módulo 11: Ciencia de Datos con Python
- Introducción a la Ciencia de Datos
- NumPy para Computación Numérica
- Pandas para Manipulación de Datos
- Matplotlib para Visualización de Datos
- Introducción al Aprendizaje Automático con scikit-learn
