Saltar al contenido principal
Avanzado1 hora
Prerequisitos: Funciones · Manejo de Errores
Módulo 07: Conceptos AvanzadosLección 3 de 4
75% completado

Context Managers en Python

🗺️ Mapa de la Lección

1 hora🔵 Avanzado

🎯 Qué aprenderás

  • Entender qué son context managers y por qué usarlos
  • Usar with statement para gestión segura de recursos
  • Crear context managers personalizados
  • Manejar recursos que necesitan limpieza
  • Aplicar context managers en casos reales

🌍 Para qué te sirve

  • Abrir y cerrar archivos automáticamente
  • Gestionar conexiones a bases de datos
  • Bloquear recursos compartidos (threading locks)
  • Configurar y restaurar estados temporales
  • Medir tiempo de ejecución de bloques de código
  • Gestionar transacciones de base de datos

🎯 ¿Por qué aprender context managers?

Probablemente ya usas context managers sin saberlo. Cuando escribes with open('archivo.txt') as f:, ese with es un context manager.

Los context managers son esenciales porque:

  • Gestión segura de recursos: garantizan limpieza automática
  • Menos errores: recursos siempre se liberan correctamente
  • Código más limpio: no necesitas recordar cerrar recursos manualmente
  • Manejo de errores: funcionan incluso si hay excepciones
  • Patrón común: usado extensivamente en Python

Sin context managers, podrías olvidar cerrar archivos, conexiones o liberar recursos, causando memory leaks o bloqueos.

🌍 Casos reales donde se usa

Los context managers están en muchos lugares de Python:

  • Abrir y cerrar archivos: Garantiza que los archivos se cierren
  • Gestionar conexiones: Bases de datos, APIs, sockets
  • Bloquear recursos: Threading locks, semáforos
  • Configurar estados temporales: Cambiar configuración y restaurarla
  • Medir tiempo: Context managers para profiling
  • Gestionar transacciones: Bases de datos, operaciones atómicas

Ejemplo real: Cuando abres un archivo con with open(), Python garantiza que se cierre incluso si hay un error. Esto previene memory leaks y problemas de archivos bloqueados.

💡 Concepto base

Los context managers garantizan que los recursos se manejen correctamente: se abren/obtienen al entrar y se cierran/liberan al salir, incluso si hay errores.

Lo genial de Python: La sintaxis with hace que los context managers sean muy legibles y seguros.

# Sin context manager (problemático)
f = open('archivo.txt', 'r')
contenido = f.read()
# ¿Qué pasa si hay un error aquí? El archivo nunca se cierra
f.close() # Esta línea puede no ejecutarse

# Con context manager (seguro)
with open('archivo.txt', 'r') as f:
contenido = f.read()
# El archivo se cierra automáticamente aquí, incluso si hay un error
print(f"Contenido: {contenido[:50]}...")
Resultado esperado:
Contenido: [primeros 50 caracteres del archivo]...
🌮 Analogía culinaria

Los context managers son como tener un asistente en la cocina que automáticamente limpia después de ti. Cuando usas el horno (with abrir_horno()), el asistente se asegura de que se apague y limpie, incluso si te distraes o algo sale mal. No importa si se quema algo o te olvidas, el horno siempre se cierra correctamente. Es como tener un sistema que garantiza que los recursos (hornos, estufas, refrigeradores) siempre se manejen correctamente, sin importar qué pase.

Para principiantes

Antes de continuar: Asegúrate de entender Funciones y Manejo de Errores. Los context managers son la forma recomendada de manejar recursos en Python. Siempre usa with cuando trabajes con archivos, conexiones o cualquier recurso que necesite limpieza.

with open('archivo.txt', 'r') as f: contenido = f.read()

El archivo SIEMPRE se cierra, incluso si hay un error


**Casos reales donde los necesitas:**
- Abrir y cerrar archivos
- Conexiones a bases de datos
- Locks (candados) para threading
- Cambios temporales de configuración
- Recursos que necesitan limpieza

**Ventaja clave:** Garantiza que la limpieza siempre ocurra, incluso si hay excepciones.

> **Antes de continuar**: Asegúrate de entender [Manejo de Errores](../05_Manejo_de_Errores_y_Buenas_Practicas/01_excepciones.md) y [Clases](../04_Programacion_Orientada_a_Objetos/01_clases_objetos.md).

## Conceptos Básicos

### Uso Básico con `with`

El `with` statement es la forma de usar context managers. La sintaxis es:

```python
with objeto_context_manager as variable:
# Código que usa el recurso
pass
# Aquí el recurso se limpia automáticamente

Ejemplo real: Abrir archivos

# Abrir y cerrar archivo
with open('archivo.txt', 'r') as f:
contenido = f.read()
# Puedes usar 'f' aquí
# El archivo se cierra automáticamente aquí, incluso si hay un error

¿Qué está pasando?

  1. open() devuelve un context manager (un objeto archivo)
  2. as f asigna el archivo abierto a la variable f
  3. El código dentro del with se ejecuta
  4. Al salir del bloque (normal o por error), Python llama automáticamente a f.close()

Ejemplo: Conexión a base de datos

# Conexión a base de datos (ejemplo conceptual)
with conexion_db() as db:
db.ejecutar_query("SELECT * FROM usuarios")
# Puedes hacer más operaciones aquí
# La conexión se cierra automáticamente aquí

Ventaja: No importa si hay un error dentro del with, la conexión siempre se cierra. Sin with, tendrías que usar try/finally manualmente.

Crear un Context Manager

class MiContextManager:
def __init__(self, nombre):
self.nombre = nombre

def __enter__(self):
print(f"Entrando al contexto: {self.nombre}")
return self

def __exit__(self, exc_type, exc_val, exc_tb):
print(f"Saliendo del contexto: {self.nombre}")
if exc_type:
print(f"Error: {exc_val}")
return True # Suprime la excepción

Ejemplos Comunes

Gestión de Archivos

class GestorArchivo:
def __init__(self, nombre_archivo, modo='r'):
self.nombre_archivo = nombre_archivo
self.modo = modo
self.archivo = None

def __enter__(self):
self.archivo = open(self.nombre_archivo, self.modo)
return self.archivo

def __exit__(self, exc_type, exc_val, exc_tb):
if self.archivo:
self.archivo.close()

# Uso
with GestorArchivo('datos.txt', 'w') as f:
f.write('Hola, mundo!')

Gestión de Conexiones

class GestorConexion:
def __init__(self, host, puerto):
self.host = host
self.puerto = puerto
self.conexion = None

def __enter__(self):
self.conexion = self.establecer_conexion()
return self.conexion

def __exit__(self, exc_type, exc_val, exc_tb):
if self.conexion:
self.cerrar_conexion()

def establecer_conexion(self):
print(f"Conectando a {self.host}:{self.puerto}")
return "Conexión establecida"

def cerrar_conexion(self):
print("Cerrando conexión")

Gestión de Locks

import threading

class GestorLock:
def __init__(self):
self.lock = threading.Lock()

def __enter__(self):
self.lock.acquire()
return self

def __exit__(self, exc_type, exc_val, exc_tb):
self.lock.release()

# Uso
lock = GestorLock()
with lock:
# Código que requiere exclusión mutua
pass

Decorador @contextmanager

Uso Básico

from contextlib import contextmanager

@contextmanager
def gestor_temporal():
print("Configuración inicial")
try:
yield
finally:
print("Limpieza final")

# Uso
with gestor_temporal():
print("Código principal")

Ejemplo Práctico

from contextlib import contextmanager
import time

@contextmanager
def medir_tiempo(nombre):
inicio = time.time()
try:
yield
finally:
fin = time.time()
print(f"{nombre}: {fin - inicio:.2f} segundos")

# Uso
with medir_tiempo("Operación costosa"):
# Código a medir
time.sleep(1)

Buenas Prácticas

  1. Manejo de Errores
class GestorSeguro:
def __enter__(self):
try:
self.recurso = self.obtener_recurso()
return self.recurso
except Exception as e:
print(f"Error al obtener recurso: {e}")
raise

def __exit__(self, exc_type, exc_val, exc_tb):
try:
self.liberar_recurso()
except Exception as e:
print(f"Error al liberar recurso: {e}")
return False # Propaga el error
  1. Recursos Anidados
class GestorRecursos:
def __init__(self, nombre):
self.nombre = nombre

def __enter__(self):
print(f"Obteniendo {self.nombre}")
return self

def __exit__(self, exc_type, exc_val, exc_tb):
print(f"Liberando {self.nombre}")

# Uso
with GestorRecursos("Recurso1") as r1:
with GestorRecursos("Recurso2") as r2:
print("Trabajando con recursos")
  1. Context Manager como Decorador
from functools import wraps

def con_reintentos(max_intentos=3):
def decorador(func):
@wraps(func)
def wrapper(*args, **kwargs):
for intento in range(max_intentos):
try:
return func(*args, **kwargs)
except Exception as e:
if intento == max_intentos - 1:
raise
print(f"Intento {intento + 1} fallido: {e}")
return wrapper
return decorador

# Uso
@con_reintentos(max_intentos=3)
def operacion_riesgosa():
# Código que puede fallar
pass

Ejemplos Avanzados

Gestión de Transacciones

class GestorTransaccion:
def __init__(self, conexion):
self.conexion = conexion

def __enter__(self):
self.conexion.begin()
return self.conexion

def __exit__(self, exc_type, exc_val, exc_tb):
if exc_type:
self.conexion.rollback()
else:
self.conexion.commit()

Gestión de Caché

from contextlib import contextmanager
import tempfile
import os

@contextmanager
def directorio_temporal():
temp_dir = tempfile.mkdtemp()
try:
yield temp_dir
finally:
for archivo in os.listdir(temp_dir):
os.remove(os.path.join(temp_dir, archivo))
os.rmdir(temp_dir)

Consejos

  1. Usa context managers para recursos que necesitan limpieza
  2. Maneja las excepciones apropiadamente
  3. Considera usar @contextmanager para casos simples
  4. Documenta el comportamiento del context manager
  5. Asegúrate de liberar recursos en __exit__

Recursos Adicionales

Documentación Oficial

Bibliografía Recomendada

  • Fluent Python (Luciano Ramalho) - Capítulo 15: Context Managers and else Blocks
  • Effective Python (Brett Slatkin) - Item 66: Consider contextlib and with Statements for Reusable try/finally Behavior
  • Python Tricks (Dan Bader) - Capítulo sobre context managers
  • Python Cookbook, 3rd Ed (Beazley & Jones) - Recetas sobre context managers

Conceptos Relacionados


Siguiente paso

Siguiente paso