Context Manager en Python



@eduardo_gpg

Número de visitas 212

Tiempo de lectura 4 min

29 Febrero 2024

Hace un par de días me encontraba leyendo un libro de Ruby para mejorar mis habilidades con el lenguaje. En uno de los capitulos se hablo de las ventajas de utilizar proc.

Si a hondar mucho en el tema, podemos definir a los procs como objetos que podemos tratar como bloques. El tema es super interesante, quizás hablemos de él en otra ocasión.

Al llegar a los ejercicios no pude dejar de lado que existe cierta similitud entre los proc y los context manager en Python, así que, siento esta una página de Python, el día de hoy me gustaría hablemos del context manager en Python. Expliquemos qué es y cómo podemos usarlo a nuestro favor.

Considero que este es un tema intermedio-avanzado, pero así que si algún concepto no queda del todo claro sabes que puedes hacer uso de la caja de comentarios.

Bien, una vez dicho todo esto, y sin más introducción, comencemos con esta nueva entrega.

Context manager.

Antes de entrar de lleno al uso de context manager, me gustaría comencemos con un ejemplo común en desarrollo.

Imaginemos que tenemos la necesidad de obtener el contenido de un archivo en nuestro sistema. Para esto, muy probablemente, usemos la función open, función que nos permite trabajar con archivos de una forma muy sencilla.

Aquí un ejemplo.

file = open('myfile.txt', 'r')

content = file.read()

file.close()

Ese código sin duda funciona, pero es muy raro de ver. La realidad es que, si vamos a trabajar con la función open lo común sea que nos apoyemos de la palabra reservada with.

with open('myfile.txt', 'r') as file:
    content = file.read()

Esto tiene muchas ventajas. La primera, y quizás la más obvia, es que podemos dejar a un lado el método close para nuestro archivo, pudiendo así evitar problemas más adelante si es que olvidamos cerrarlo. Además de esto nuestro objeto file tiene un scope mucho más limitado. Al este encontrarse definido con with su ciclo de vida se limita únicamente al bloque.

Para este segundo ejemplo el método open retorna un context manager. Y, bueno, ¿Qué es un context manager?

Podemos definir un context manager como un objeto en Python diseñado para ser usado junto con la palabra reservada with. Un context manager nos permite encapsular acciones que deseamos se ejecuten antes o después de ciertas declaraciones. Siendo estas declaraciones (stataments) las que se encuentran en el bloque del with.

Si bien es cierto el lenguaje ya nos ofrece un par de funciones/objectos context manager, también es importante destacar que podemos crear los nuestros.

Para esto hay que tomar en cuenta 2 cosas.

1.- Todos los objetos context manager que creemos, obligatoriamente, deben tener los métodos enter y exit.

2.- Todos los objetos context manager serán usados en conjunto con la palabra reservada with.

Aquí te explico cómo funcionan los métodos

  • enter(): Este método es llamado al comienzo del bloque with. Podemos usarlo para inicializar variables o realizar x o y tareas. Obligatoriamente este método retornar un objeto. Este objeto es el que se almacena en la variable definida que se encuentra después de la palabra reservada as.
  • exit(exc_type, exc_val, exc_tb): Este bloque es llamado al finalizar el bloque with. Inclusive si existe, o no, algún tipo de exception. Este método es el encargado de realizar tareas de teardown, es decir, tareas de cierre, cómo lo pude ser liberar memoria, cerrar archivos, borrar registros etc..

Veamos un ejemplo para que nos quede más en claro. Creemos un context manager que nos permita conocer cuanto tiempo le toma finalizar a un bloque.

El código puede quedar de la siguiente manera.

import time

class ExecutionTimeTracker:
    def __enter__(self, name):
        self.name = name
        self.end_time= None
        self.start_time = time.time()

        return self 

    def __exit__(self, exc_type, exc_val, exc_tb):
        self.end_time = time.time()
        self.elapsed = self.end_time - self.start_time

        print(f"{self.name} execution took {self.elapsed} seconds")

        return False

with ExecutionTimeTracker('Example Context'):
    for i in range(1000000):
        ...

Para este ejemplo puede observar como en la función enter inicializamos variable, y para este caso en particular retornamos el mismo objeto.

En el método exit simplemente terminamos el bloque conociendo la diferencia de tiempos desde cuando comenzó hasta cuando termino.

Un código, desde mi punto de vista bastante cool.

Por supuesto, el código que se encuentre en el bloque with puede ser reemplazado por el que tú desees.

Context manager as decorator.

En el código anterior pudimos crear un objeto contex manager usando nuestra clase con sus correspondientes dunder methods. El código funciona, peeero, la verdad nunca he visto algo parecido en producción. En el mundo real (Por lo menos lo que yo he visto) la forma más rápida y eficiente de crear este tipo de objetos es usando el decorador contextmanager.

Veamos como quedaría nuestro ejemplo con este decorador.

from contextlib import contextmanager
import time

@contextmanager
def execution_time_tracker(name):
    try:
        start_time = time.time()
        yield 
    finally:
        end_time = time.time()
        elapsed = end_time - start_time
        print(f"{name} Execution took {elapsed} seconds")

with execution_time_tracker():
    for i in range(1000000):
        pass 

Ahora, usando el decorador contextemanager, es a través de una función que podemos definir acciones antes o después de los statements. Y sí, los statements se ejecutarán cuando la función pierda el control en la palabra reservada yield.

Si nuestro contexto debe retornar un objeto lo hará junto con la palabra reservada yield.

El código queda aun mejor. Mucho más limpio. Fácil de leer y fácil de mantener.

Yo te hago la invitación a que, siempre que puedas, y sea necesario, hagas uso del decorador contextmanager para definir acciones antes o después de ciertos statementes. Nuestro código quedara mucho más pythonico. Algo muy parecido a los decoradores, pero ahora con sentencias.

Conclusión.

Para este artículo nos enfocamos en un context manager relativamente sencillo, sin embargo estos puede ser mucho más complejos. Por ejemplo, podemos crear un context manager que mantenga activa una conexión a una base de datos, mantenga actualizado hilos de una pool o se encuentre monitorizando otros procesos del sistema.

Los limites los definen tú y tu equipo.

from contextlib import contextmanager

@contextmanager
def managed_session():

    session = MySuperDBConnection()

    try:
        yield session

    except:
        raise

    finally:
        session.close()

with managed_session() as session:
    ...

¿El contenido te resulto de ayuda?

Para poder dejar tu opinión es necesario ser un usuario autenticado. Login

Más Tips y Ejercicios 🐍

Adquiere una subscripción PyWombat por tan solo $3 USD. al mes.

Conoce los beneficios de ser usuario premium:
Niveles desbloqueados: Ten accesos a todos los niveles de ejercicios. 🔓
Nuevo límite: Incrementa tu límite de ejercicios por semana. 🚀
Contenido único: Recibe semanalmente recursos exclusivos de Python (Videos, Artículos y Capitulos del libro PyWombat, comienza como desarrollador Python. 🐍