¿Qué son los Context Managers?
Los context managers son objetos que definen el contexto de ejecución para un bloque de código. Garantizan que los recursos se configuren correctamente y se limpien automáticamente, incluso si ocurre un error.
El Statement 'with'
Problema sin 'with'
# Forma tradicional (propensa a errores)
archivo = open('datos.txt', 'r')
try:
contenido = archivo.read()
print(contenido)
finally:
archivo.close() # Asegurar que se cierre
# ¿Qué pasa si olvidas cerrar el archivo?
archivo = open('datos.txt', 'r')
contenido = archivo.read()
# ¡Archivo nunca se cierra! (memory leak)Solución con 'with'
# Forma pythonica (segura y elegante)
with open('datos.txt', 'r') as archivo:
contenido = archivo.read()
print(contenido)
# El archivo se cierra automáticamente al salir del bloqueCómo Funciona 'with'
# Cuando usas with, Python hace:
# 1. Llama al método __enter__() del context manager
# 2. Ejecuta el bloque de código
# 3. Llama al método __exit__() (siempre, incluso con errores)
with expresion as variable:
# código
# Equivale a:
manager = expresion
variable = manager.__enter__()
try:
# código
finally:
manager.__exit__()Context Managers Incorporados
1. Archivos
# Leer archivo
with open('entrada.txt', 'r', encoding='utf-8') as archivo:
contenido = archivo.read()
print(contenido)
# Escribir archivo
with open('salida.txt', 'w', encoding='utf-8') as archivo:
archivo.write("Contenido\n")
archivo.write("Más contenido\n")
# Múltiples archivos
with open('entrada.txt', 'r') as entrada, open('salida.txt', 'w') as salida:
for linea in entrada:
salida.write(linea.upper())2. Locks (Threading)
import threading
lock = threading.Lock()
# Sin context manager
lock.acquire()
try:
# código crítico
pass
finally:
lock.release()
# Con context manager
with lock:
# código crítico
pass
# El lock se libera automáticamente3. Conexiones de Base de Datos
import sqlite3
# Conexión se cierra automáticamente
with sqlite3.connect('base_datos.db') as conn:
cursor = conn.cursor()
cursor.execute('SELECT * FROM usuarios')
resultados = cursor.fetchall()
# Si hay error, hace rollback automático
# Si no hay error, hace commit automáticoCrear Context Manager con Clase
class ManejarArchivo:
def __init__(self, nombre_archivo, modo):
self.nombre_archivo = nombre_archivo
self.modo = modo
self.archivo = None
def __enter__(self):
print(f"Abriendo {self.nombre_archivo}")
self.archivo = open(self.nombre_archivo, self.modo)
return self.archivo
def __exit__(self, exc_type, exc_val, exc_tb):
print(f"Cerrando {self.nombre_archivo}")
if self.archivo:
self.archivo.close()
# Manejar excepciones
if exc_type is not None:
print(f"Ocurrió un error: {exc_val}")
# Return False propaga la excepción
# Return True suprime la excepción
return False
# Usar el context manager
with ManejarArchivo('test.txt', 'w') as archivo:
archivo.write("Hola mundo")
# Salida:
# Abriendo test.txt
# Cerrando test.txtContext Manager con @contextmanager
from contextlib import contextmanager
@contextmanager
def manejar_archivo(nombre, modo):
print(f"Abriendo {nombre}")
archivo = open(nombre, modo)
try:
yield archivo # Punto donde se ejecuta el bloque 'with'
finally:
print(f"Cerrando {nombre}")
archivo.close()
# Usar
with manejar_archivo('test.txt', 'w') as f:
f.write("Contenido")Ejemplos Prácticos
1. Timer Context Manager
import time
from contextlib import contextmanager
@contextmanager
def timer(nombre="Operación"):
inicio = time.time()
print(f"Iniciando {nombre}...")
yield
fin = time.time()
print(f"{nombre} completada en {fin - inicio:.4f} segundos")
# Usar
with timer("Procesamiento de datos"):
time.sleep(1)
total = sum(range(1000000))
# Salida:
# Iniciando Procesamiento de datos...
# Procesamiento de datos completada en 1.0234 segundos2. Cambiar Directorio Temporalmente
import os
from contextlib import contextmanager
@contextmanager
def cambiar_directorio(ruta):
directorio_original = os.getcwd()
print(f"Cambiando de {directorio_original} a {ruta}")
os.chdir(ruta)
try:
yield
finally:
print(f"Regresando a {directorio_original}")
os.chdir(directorio_original)
# Usar
print(f"Directorio actual: {os.getcwd()}")
with cambiar_directorio('/tmp'):
print(f"Directorio temporal: {os.getcwd()}")
# Hacer operaciones en /tmp
print(f"De vuelta en: {os.getcwd()}")3. Suprimir Excepciones Específicas
from contextlib import contextmanager
@contextmanager
def ignorar_excepciones(*excepciones):
try:
yield
except excepciones as e:
print(f"Excepción ignorada: {e}")
# Usar
with ignorar_excepciones(FileNotFoundError, ValueError):
archivo = open('archivo_inexistente.txt')
# No causa error, la excepción es ignorada
print("Programa continúa")4. Database Transaction Manager
from contextlib import contextmanager
import sqlite3
@contextmanager
def transaccion(conexion):
cursor = conexion.cursor()
try:
yield cursor
conexion.commit()
print("Transacción completada")
except Exception as e:
conexion.rollback()
print(f"Transacción revertida: {e}")
raise
# Usar
conn = sqlite3.connect(':memory:')
conn.execute('CREATE TABLE usuarios (id INTEGER, nombre TEXT)')
with transaccion(conn) as cursor:
cursor.execute('INSERT INTO usuarios VALUES (1, "Ana")')
cursor.execute('INSERT INTO usuarios VALUES (2, "Luis")')
# Si hay error aquí, se hace rollback automático5. Redirect Output
import sys
from contextlib import contextmanager
@contextmanager
def redirect_stdout(nuevo_destino):
antiguo_stdout = sys.stdout
sys.stdout = nuevo_destino
try:
yield
finally:
sys.stdout = antiguo_stdout
# Usar - redirigir a archivo
with open('output.txt', 'w') as f:
with redirect_stdout(f):
print("Esto va al archivo")
print("Esto también")
print("Esto va a la consola")6. Temporary File Manager
import os
from contextlib import contextmanager
@contextmanager
def archivo_temporal(nombre):
print(f"Creando archivo temporal: {nombre}")
with open(nombre, 'w') as f:
f.write("Contenido temporal")
try:
yield nombre
finally:
print(f"Eliminando archivo temporal: {nombre}")
if os.path.exists(nombre):
os.remove(nombre)
# Usar
with archivo_temporal('temp.txt') as archivo:
print(f"Trabajando con {archivo}")
with open(archivo, 'r') as f:
print(f.read())
# El archivo se elimina automáticamenteNested Context Managers
# Forma tradicional
with open('archivo1.txt', 'r') as f1:
with open('archivo2.txt', 'w') as f2:
contenido = f1.read()
f2.write(contenido.upper())
# Forma más limpia (Python 3.1+)
with open('archivo1.txt', 'r') as f1, \
open('archivo2.txt', 'w') as f2:
contenido = f1.read()
f2.write(contenido.upper())
# Usando contextlib.ExitStack
from contextlib import ExitStack
archivos = ['file1.txt', 'file2.txt', 'file3.txt']
with ExitStack() as stack:
files = [stack.enter_context(open(fname)) for fname in archivos]
# Trabajar con todos los archivos
for f in files:
print(f.read())
# Todos se cierran automáticamenteContext Manager para Testing
from contextlib import contextmanager
@contextmanager
def assert_raises(exception):
try:
yield
except exception:
print(f"Excepción {exception.__name__} capturada correctamente")
else:
raise AssertionError(f"Se esperaba {exception.__name__} pero no ocurrió")
# Usar en tests
with assert_raises(ValueError):
int("abc") # Esto lanza ValueError
# with assert_raises(ValueError):
# int("123") # Esto NO lanza ValueError - falla el testSuprimir Warnings
import warnings
from contextlib import contextmanager
@contextmanager
def suprimir_warnings():
with warnings.catch_warnings():
warnings.simplefilter("ignore")
yield
# Usar
with suprimir_warnings():
# Código que genera warnings
warnings.warn("Este warning no se mostrará")Context Manager para API
import requests
from contextlib import contextmanager
@contextmanager
def api_session(base_url, token):
session = requests.Session()
session.headers.update({'Authorization': f'Bearer {token}'})
session.base_url = base_url
try:
yield session
finally:
session.close()
print("Sesión API cerrada")
# Usar
# with api_session('https://api.ejemplo.com', 'mi_token') as session:
# response = session.get('/users')
# print(response.json())Ventajas de Context Managers
- Gestión automática de recursos: Garantiza limpieza incluso con errores
- Código más limpio: Elimina bloques try-finally repetitivos
- Previene leaks: Archivos, conexiones, locks siempre se liberan
- Más legible: Claramente define el alcance del recurso
- Pythonic: Sigue las mejores prácticas de Python
Cuándo Usar Context Managers
- Manejo de archivos
- Conexiones de red/base de datos
- Locks y semáforos
- Cambios temporales de estado
- Medición de tiempo
- Redirección de I/O
- Transacciones
Buenas Prácticas
- Siempre libera recursos: En el __exit__ o finally
- Maneja excepciones apropiadamente: Decide si suprimir o propagar
- Documenta el comportamiento: Explica qué hace el context manager
- Usa @contextmanager cuando sea simple: Para casos complejos, usa clase
- Testing: Prueba casos normales y con excepciones
Conclusión
Los context managers son una característica esencial de Python que garantiza la gestión correcta de recursos. El statement 'with' hace tu código más seguro, limpio y pythonic. Ya sea usando context managers incorporados o creando los tuyos propios, dominar esta técnica es crucial para escribir código profesional en Python.