programa
Motivación
Puede que ya conozcas la sentencia with, una forma concisa de gestionar recursos en Python. Pero, ¿te has preguntado alguna vez cómo funciona por dentro? La fuerza de la palabra clave with viene de los gestores de contexto.
Los gestores de contexto son un patrón de diseño fundamental en Python que aporta una forma estructurada de gestionar recursos. Garantizan que los recursos se adquieran, se usen correctamente y, por último, se liberen o limpien, incluso cuando hay errores o excepciones.
Esto es especialmente importante al trabajar con recursos como ficheros, conexiones de red o manejadores de bases de datos. Gracias a los gestores de contexto, podemos escribir código más limpio y fiable, sin preocuparnos por olvidar cerrar un archivo o liberar un bloqueo.
En este tutorial, iremos más allá de los gestores de contexto que ofrece Python por defecto y aprenderemos a escribir los nuestros.
Entender los gestores de contexto en Python
Bajo el capó, los gestores de contexto son objetos que definen dos métodos especiales: __enter__ y __exit__. El método __enter__ se llama al entrar en el bloque with y su valor de retorno se asigna a una variable dentro de ese bloque. El método __exit__, por su parte, se llama cuando se sale del bloque with, tanto si termina con normalidad como si lo hace con una excepción.
Esta estructura garantiza un manejo correcto de los recursos. Veamos algunos gestores de contexto integrados en Python para entenderlo mejor:
Tomemos el ejemplo clásico de abrir un archivo:
with open('file.txt', 'r') as file:
data = file.read()
Aquí, open('file.txt', 'r') actúa como gestor de contexto. Al entrar en el bloque with, se llama al método enter del objeto archivo, se abre el fichero y se asigna a la variable file. Después puedes usar file.read() para acceder a su contenido.
A continuación, se garantiza que el método __exit__ del objeto archivo se llame al salir del bloque with, incluso si se produce una excepción. Este método se encarga de cerrar el archivo, evitando dejar descriptores abiertos por ahí.
2. Bloqueo de hilos
Más allá de los archivos, los gestores de contexto también pueden usarse para la sincronización de hilos con threading.Lock():
import threading
lock = threading.Lock()
with lock:
# Sección crítica
print("This code is executed under lock protection.")
Aquí, lock es un objeto threading.Lock, otro gestor de contexto. Al entrar en el bloque with, el método __enter__ adquiere el bloqueo, garantizando que solo un hilo pueda ejecutar la sección crítica a la vez. Finalmente, el método __exit__ libera el bloqueo al salir del bloque with, permitiendo continuar a otros hilos.
3. Conexiones a bases de datos
De forma similar, los gestores de contexto pueden gestionar conexiones a bases de datos:
import sqlite3
with sqlite3.connect('database.db') as connection:
cursor = connection.cursor()
cursor.execute("SELECT * FROM table")
rows = cursor.fetchall()
La llamada sqlite3.connect('database.db') es un gestor de contexto. Al entrar en el bloque with se establece la conexión a la base de datos y se asigna a connection. Después puedes usar un cursor para interactuar con la base. El método __exit__ garantiza que la conexión se cierre al salir del bloque with, evitando fugas de recursos.
4. Sockets de red
Los gestores de contexto incluso pueden encargarse de la comunicación de red:
import socket
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.connect(('localhost', 8080))
s.sendall(b'Hello, world')
data = s.recv(1024)
Aquí, socket.socket(socket.AF_INET, socket.SOCK_STREAM) crea un objeto socket que actúa como gestor de contexto. El método __enter__ crea el socket y, dentro del bloque with, puedes conectar, enviar y recibir datos. El método __exit__ garantiza que el socket se cierre correctamente al finalizar.
5. Exploración de directorios
En este caso, os.scandir('.') permite iterar por las entradas de un directorio:
with os.scandir('.') as entries:
for entry in entries:
print(entry.name)
os.scandir('.') actúa aquí como gestor de contexto. El método __enter__ abre el escaneo del directorio y puedes iterar por las entradas dentro del bloque with. El método __exit__ limpia el escaneo al salir.
Como ves, la sentencia with, impulsada por los gestores de contexto, simplifica la gestión de recursos al encargarse automáticamente de su asignación y liberación.
Crear gestores de contexto personalizados en Python
Veamos ahora cómo escribir tus propios gestores para tener un control más fino sobre la gestión de recursos.
Hay dos enfoques principales para escribir gestores de contexto personalizados: basados en clases y basados en funciones.
Enfoque basado en clases
El enfoque basado en clases es el método más estructurado y flexible para escribir gestores de contexto. Aquí defines una clase que implementa los métodos especiales __enter__ y __exit__.
Veamos un ejemplo de una clase Timer que mide el tiempo de ejecución:
import time
class Timer:
def __enter__(self):
self.start_time = time.time()
return self
def __exit__(self, exc_type, exc_value, traceback):
self.end_time = time.time()
elapsed_time = self.end_time - self.start_time
print(f"Elapsed time: {elapsed_time} seconds")
# Example usage
if __name__ == "__main__":
with Timer() as timer:
# Code block to measure the execution time
time.sleep(2) # Simulate some time-consuming operation
Elapsed time: 2.002082347869873 seconds
Aquí, la clase Timer define el método __enter__ para capturar la hora de inicio al entrar en el bloque with. Devuelve self para permitir el acceso al objeto dentro del bloque. El método __exit__ calcula el tiempo transcurrido al salir del bloque with y lo imprime.
Enfoque basado en funciones
Si prefieres algo más conciso, aunque quizá menos flexible, el enfoque basado en funciones puede ser una buena opción. Aquí usas el decorador contextmanager del módulo contextlib para convertir una función en gestor de contexto.
Veamos cómo funciona:
import time
from contextlib import contextmanager
@contextmanager
def timer():
start_time = time.time()
yield
end_time = time.time()
elapsed_time = end_time - start_time
print(f"Elapsed time: {elapsed_time} seconds")
# Example usage
if __name__ == "__main__":
with timer():
time.sleep(2)
Elapsed time: 2.0020740032196045 seconds
El decorador @contextmanager transforma la función timer en un gestor de contexto. Dentro de la función se captura start_time, y la sentencia yield pausa la ejecución para que se ejecute el código del bloque with.
Por último, la funcionalidad equivalente a __exit__ se consigue capturando la hora final e imprimiendo el tiempo transcurrido.
En esencia, escribes la lógica de __enter__ antes de la palabra clave yield, mientras que la lógica de __exit__ va después. Ambos enfoques logran lo mismo; la elección depende de tu preferencia por la estructura y la legibilidad.
Ejemplos prácticos de gestores de contexto
Ahora que sabes escribir gestores de contexto personalizados en Python, veamos algunos ejemplos prácticos donde pueden ser muy útiles.
Gestionar operaciones con archivos
Las operaciones con archivos son habituales en muchas aplicaciones. Ya sea leer o escribir, una buena gestión garantiza la integridad de los datos y la eficiencia de los recursos. Consideremos un escenario en el que queremos escribir texto a un archivo usando un gestor de contexto personalizado.
class FileManager:
def __init__(self, filename, mode):
self.filename = filename
self.mode = mode
self.file = None
def __enter__(self):
self.file = open(self.filename, self.mode)
return self.file
def __exit__(self, exc_type, exc_value, traceback):
if self.file:
self.file.close()
# Example usage
if __name__ == "__main__":
with FileManager("example.txt", "w") as file:
file.write("Hello, world!\n")
En este ejemplo, definimos una clase FileManager que imita la función open de Python y gestiona las operaciones con archivos. Desglosando cada método:
__init__(self, filename, mode): es el constructor de la clase. Inicializa el objetoFileManagercon el nombre de archivo y el modo, que especifican qué archivo abrir y en qué modo (por ejemplo, lectura, escritura, anexar).__enter__(self): se llama al entrar en el bloquewith. Abre el archivo indicado porfilenameen el modo indicado pormode. El objeto fichero abierto se asigna aself.filey se devuelve para usarse dentro del bloquewith.__exit__(self, exc_type, exc_value, traceback): se llama al salir del bloque with, tanto si se produjo una excepción como si no. Garantiza que el archivo se cierre correctamente llamando aself.file.close()siself.fileno esNone. Así se evitan fugas de recursos y se asegura la limpieza.
Si inspeccionas el contenido de example.txt tras ejecutar el código anterior, verás el texto escrito:
$ cat example.txt
Hello, world!
Por supuesto, la ventaja de la clase FileManager es que puedes implementar cualquier lógica personalizada para trabajar con archivos. Por ejemplo, puedes modificarla para que lleve un registro en un fichero de texto de qué archivos se abren o cierran.
import datetime
class FileManager:
def __init__(self, filename, mode, log_filename="file_log.txt"):
self.filename = filename
self.mode = mode
self.log_filename = log_filename
self.file = None
def log_action(self, action):
timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
with open(self.log_filename, "a") as log_file:
log_file.write(f"{timestamp} - {action}: {self.filename}\n")
def __enter__(self):
self.file = open(self.filename, self.mode)
self.log_action("Opened")
return self.file
def __exit__(self, exc_type, exc_value, traceback):
if self.file:
self.file.close()
self.log_action("Closed")
# Example Usage
with FileManager("example.txt", "r") as file:
content = file.read()
print(content)
Hello, world!
En esta versión modificada:
- Se importa el módulo
datetimepara trabajar con marcas de tiempo. - Se añade el método
log_actionpara registrar acciones sobre archivos. - Se añade el parámetro
log_filenamepara indicar el nombre del archivo de registro. El valor por defecto es'file_log.txt'. - El nombre de archivo y la acción (abierto o cerrado) se añaden al registro usando el método
log_actiondentro de los métodos__enter__y__exit__. - Dentro de
log_action, se usadatetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")para obtener la marca temporal en el formato"YYYY-MM-DD HH:MM:SS". - La marca temporal se incluye en el registro junto con el nombre de archivo y la acción.
Ahora, si imprimimos el contenido de file_log.txt, veremos los registros:
$ cat file_log.txt
2024-04-03 12:37:02 - Opened: example.txt
2024-04-03 12:37:02 - Closed: example.txt
Otro caso de uso práctico que podemos implementar como gestor de contexto personalizado es una herramienta de gestión de bases de datos SQLite:
import sqlite3
class DatabaseConnection:
def __init__(self, database_name):
self.database_name = database_name
self.connection = None
def __enter__(self):
self.connection = sqlite3.connect(self.database_name)
return self.connection
def __exit__(self, exc_type, exc_value, traceback):
if self.connection:
self.connection.close()
De nuevo, esto imita la función sqlite3.connect, pero puede mejorarse añadiendo lógica personalizada de gestión de la base de datos. Comprobemos si el gestor funciona con algunas funciones de ejemplo:
# Example usage
def create_table():
with DatabaseConnection("example.db") as connection:
cursor = connection.cursor()
cursor.execute(
"""CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY,
username TEXT,
email TEXT)"""
)
def insert_data(username, email):
with DatabaseConnection("example.db") as connection:
cursor = connection.cursor()
cursor.execute(
"INSERT INTO users (username, email) VALUES (?, ?)", (username, email)
)
connection.commit()
def fetch_data():
with DatabaseConnection("example.db") as connection:
cursor = connection.cursor()
cursor.execute("SELECT * FROM users")
return cursor.fetchall()
if __name__ == "__main__":
create_table()
insert_data("john_doe", "john@example.com")
insert_data("jane_doe", "jane@example.com")
users = fetch_data()
print("Users in the database:")
for user in users:
print(user)
Users in the database:
(1, 'john_doe', 'john@example.com')
(2, 'jane_doe', 'jane@example.com')
(3, 'john_doe', 'john@example.com')
(4, 'jane_doe', 'jane@example.com')
Podemos escribir un código mucho más mantenible separando las operaciones de base de datos en funciones y usando el gestor personalizado que acabamos de crear.
Temas avanzados sobre gestores de contexto en Python
En esta sección, hablaremos de temas avanzados relacionados con los gestores de contexto en Python. Entre ellos, el manejo de errores y el anidamiento de gestores de contexto.
Manejo de errores
El manejo de errores es esencial en programación, y los gestores de contexto en Python facilitan gestionar errores dentro de operaciones de gestión de recursos.
En el ejemplo de la clase FileManager, el manejo de errores puede implementarse dentro del método __exit__ para asegurar la limpieza incluso si ocurre una excepción. Por ejemplo, si se produce un error al escribir en el archivo (dentro del bloque with), aun así queremos asegurarnos de que el archivo se cierra para evitar fugas de recursos. Podemos modificar __exit__ así:
def __exit__(self, exc_type, exc_value, traceback):
if self.file:
self.file.close()
if exc_type is not None:
print(f"An error occurred: {exc_value}")
Aquí, primero cerramos el archivo con self.file.close() para asegurar la limpieza. Luego comprobamos si ocurrió una excepción (exc_type is not None). Si es así, imprimimos un mensaje de error para dar contexto de lo sucedido.
Aquí:
exc_typees la clase de la excepción, comoAttributeErroroIndexError.exc_valuees la instancia de la excepción con su mensaje.tracebackes el traceback completo del error.
Ejecutemos la clase FileManager con un código incorrecto a propósito:
with FileManager("example.txt", "r") as file:
# Simular un error de ejemplo
content = file.read()
print(content[150])
An error occurred: string index out of range
Así, para implementar lógica personalizada al manejar excepciones en tus gestores, aquí tienes un flujo de trabajo de ejemplo que puedes seguir:
def __exit__(self, exc_type, exc_value, traceback):
# Realizar limpieza
...
# Gestionar la excepción
if isinstance(exc_value, AttributeError):
# Gestionar AttributeError aquí
print(f"Exception message: {exc_value}")
# Gestionar otra excepción
elif isinstance(exc_value, AnotherError):
...
Anidar gestores de contexto
Anidar gestores de contexto te permite gestionar varios recursos a la vez, cada uno con su propio gestor, dentro de una sola sentencia with. Esto es especialmente útil en operaciones complejas que implican múltiples recursos. Veamos cómo anidarlos en nuestros ejemplos.
Imagina que quieres abrir un archivo y realizar operaciones de base de datos dentro del mismo contexto. Por ejemplo, introducir el contenido de un CSV como filas de una base de datos. Podemos conseguirlo anidando los gestores FileManager y DatabaseConnection dentro de una sola sentencia with:
if __name__ == "__main__":
with FileManager("example.txt", "w") as file, DatabaseConnection("example.db") as connection:
...
En este ejemplo, los gestores FileManager y DatabaseConnection se anidan dentro de la misma sentencia with. Esto permite abrir el archivo y establecer la conexión a la base de datos a la vez. Las operaciones posteriores de escritura y base de datos se realizan dentro de este contexto combinado.
Algo a tener en cuenta es la legibilidad del código. Te recomendaría no exceder dos contextos simultáneos.
Conclusión
Los gestores de contexto en Python ofrecen una forma estructurada de gestionar recursos dentro de una sentencia with, garantizando su correcta asignación y liberación.
A lo largo de este tutorial, hemos visto los fundamentos de los gestores de contexto, ejemplos prácticos y temas avanzados como el manejo de errores y el anidamiento. Al dominar la creación de gestores de contexto personalizados, podrás escribir código más limpio y fiable, mejorar el manejo de errores y gestionar recursos con eficiencia.
El tema de escribir gestores de contexto personalizados entra dentro de la categoría de «Python avanzado». Aquí tienes algunos recursos relacionados para seguir desarrollando tus habilidades:
Soy creador de contenidos sobre ciencia de datos con más de 2 años de experiencia y uno de los mayores seguimientos en Medium. Me gusta escribir artículos detallados sobre IA y ML con un toque sarcástico, porque hay que darle algo de vidilla al tema. He publicado más de 130 artículos y un curso en DataCamp, y tengo otro en marcha. Mis contenidos han sido vistos por más de 5 millones de personas; 20.000 de ellas se convirtieron en seguidores tanto en Medium como en LinkedIn.

