Context Managers en Python: Domina el Statement 'with'

👤 Admin 📅 3 de noviembre, 2025 ⏱ 16 min 🏷 Python Avanzado

¿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 bloque

Có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áticamente

3. 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ático

Crear 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.txt

Context 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 segundos

2. 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ático

5. 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áticamente

Nested 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áticamente

Context 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 test

Suprimir 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.