Link Search Menu Expand Document

Gestores de contexto

Tal vez nunca hayas o铆do hablar de los gestores de contexto o context managers, pero si has trabajado con ficheros ya los has usado sin darte cuenta. Si alguna vez has visto la cl谩usula with, todo lo que pasa por debajo hace uso de los gestores de contexto.

Realmente no ofrecen ninguna funcionalidad nueva, pero permiten ahorrar c贸digo eliminando todo lo que sea repetitivo o boilerplate. En pocas palabras, permiten ejecutar dos tareas de manera autom谩tica, la primera al entrar al with y la segunda al salir del mismo.

El ejemplo m谩s t铆pico es el siguiente. Abrimos un fichero, escribimos contenido en 茅l, y lo cerramos.

# Haciendo uso de los context managers
with open('fichero.txt', 'w') as fichero:
    fichero.write('Hola!')

驴C贸mo que lo cerramos? Pues s铆, aunque no se especifique expresamente, por debajo Python ejecutar谩 la funci贸n close() cuando se salga del bloque with. Es importante notar tambi茅n que la variable fichero no ser谩 accesible fuera del with, 煤nciamente dentro.

El siguiente c贸digo es totalmente equivalente al anterior, pero sin hacer uso de los context managers, simplemente de las excepciones.

# Sin usar los context managers
fichero = open('fichero.txt', 'w')
try:
    fichero.write('Hola!')
finally:
    fichero.close()

Como puedes ver, nos podemos ahorrar algunas l铆neas de c贸digo usando los gestores de contexto. Su uso tambi茅n nos permite dotar a nuestro c贸digo de mayor expresivadad, una de las grandes ventajas de Python.

Su uso se extiende tambi茅n a otras clases como Lock y es tambi茅n com煤n verlos en bases de datos. Siempre que tengamos unos recursos que son bloqueados para ser usados, y despu茅s necesiten ser liberados pase lo que pase (aunque ocurra una excepci贸n), los gestores de contexto ser谩n una buena idea.

Llegados a este punto ya sabemos usar los gestores de contexto que vienen con Python, pero 驴y si quisi茅ramos definir uno nosotros? A continuaci贸n lo vemos.

Implementaci贸n con clases

Si quieres definir tu propio gestor de contexto, existen dos formas de hacerlo:

  • Con una clase: Implementando los m茅todos dunder __enter__ y __exit__ en tu clase.
  • Con decoradores: Usando el decorador @contextmanager.

Veamos la primera forma usando clases. Lo primero que tenemos que hacer es definir nuestra clase, e implementar los siguientes m茅todos:

  • __init__: Este m茅todo es llamado autom谩ticmente al entrar al bloque with. Lo que devuelva este m茅todo ser谩 asignado a la variable que especifiquemos a continuaci贸n del as. Es com煤n que esto sea el recurso que vamos a utilizar, un fichero por ejemplo.
  • __exit__: Este m茅todo ser谩 llamado al salir del with y contiene las tareas de limpieza que queremos ejecutar. Lo m谩s importante es que este m茅todo se llama siempre, incluso aunque ocurra una excepci贸n. Ser铆a por lo tanto equivalente al uso del bloque except. Trabajando con ficheros, aqu铆 se cerrar铆a el archivo que ha sido abierto anteriormente.

Veamos un ejemplo. Implementamos los m茅todos descritos con un simple print() para ver lo que pasa. Podemos ver como efectivamente son llamados.

class MiGestor:
    def __enter__(self):
        print("Entra en __enter__")
    def __exit__(self, exc_type, exc_value, traceback):
        print("Entra en __exit__")

with MiGestor() as f:
    print("Hola")
    
# Entra en __enter__
# Hola
# Entra en __exit__

Como se puede ver, Python llama por debajo a ambos m茅todos, primero al __enter__ y despu茅s al __exit__.

Vamos a complicarlo un poco m谩s. Como hemos indicado, el m茅todo __exit__ es ejecutado aunque ocurra una excepci贸n. Vamos por lo tanto a forzar una y ver que pasa.

with MiGestor() as f:
    raise Exception
    
# Entra en __enter__
# Entra en __exit__

Como era de esperar, el contenido del m茅todo __exit__ tambi茅n es ejecutado. Tal vez te hayas fijado en los atributos de entrada del m茅todo. Son usados para obtener m谩s informaci贸n sobre la excepci贸n que ocurri贸 y poder actuar en consecuencia. Son los siguientes:

  • exc_type: Tipo de excepci贸n que fue lanzada. En nuestro ejemplo ser铆a <class 'Exception'>
  • exc_value: Valor de la excepci贸n en el caso de que fuera proporcionado.
  • traceback: Objecto traceback con m谩s informaci贸n acerca de la excepci贸n.

Una vez sabido esto, ya estamos en condiciones de implementar nuestro propio gestor de contexto con un ejemplo un poco m谩s realista. Vamos a crear nuestro propia clase que envuelva a un fichero con un gestor de contexto. Esta clase abrir谩 y cerrar谩 un fichero haciendo uso de los gestores de contexto.

class MiClaseFichero:
    def __init__(self, nombre_fichero):
        self.nombre_fichero = nombre_fichero

    def __enter__(self):
        self.fichero = open(self.nombre_fichero, 'w')
        return self.fichero

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

Vayamos parte por parte:

  • En el __init__ guardamos el nombre del fichero que queremos crear, nada nuevo.
  • En el __enter__ creamos un fichero, lo almacenamos en nuestra clase, y devolvemos la referencia que ser谩 usada dentro del with.
  • Por 煤ltimo en el __exit__ cerramos el fichero si estaba abierto.

Una vez definida la clase, ya estamos en condiciones de usarla como hemos visto anteriormente.

with MiClaseFichero("fichero.txt") as fichero:
    fichero.write("Hola!")

Por supuesto se trata de un ejemplo did谩ctico, si quieres leer un fichero simplemente usa las funciones que Python proporciona por defecto.

Implementaci贸n con decoradores

La programaci贸n orientada a objetos es muy 煤til, pero no conviene abusar de ella. Tal vez te encuentres en una situaci贸n donde no sea realmente necesario crear una clase. Por suerte, tambi茅n podemos definir gestores de contexto con decoradores.

Para ello puedes usar la librer铆a contextlib. Su uso es muy similar pero tal vez sea un poco m谩s complejo si no conoces los generadores y el uso del yield.

from contextlib import contextmanager

@contextmanager
def gestor_fichero(nombre_fichero):
    try:
        fichero = open(nombre_fichero, 'w')
        yield fichero
    finally:
        fichero.close()

Como puedes ver, el contenido del try ser铆a el equivalente al contenido del __enter__ y el finally al del __exit__. Una vez tenemos definida nuestra funci贸n, podemos usarla de la misma forma que hemos visto anteriormente.

with gestor_fichero("fichero.txt") as fichero:
    fichero.write("Hola!")

Anidando diferentes with

Es posible tambi茅n anidar diferentes with, es decir, realizar una nueva llamada al with sin haber salido del bloque anterior.

Esto puede dar lugar a c贸digos de lo m谩s creativos como el que mostramos a continuaci贸n. Se trata de un generador de 铆ndices, como el que se podr铆a encontrar en un libro. Cada vez que se crea un nuevo bloque with, se a帽ade un nuevo nivel y se van numerando de cero a n, lo que modifica la funci贸n print.

class Indice:
    def __init__(self):
        self.level = -1
        self.numeracion = [0]

    def __enter__(self):
        self.level += 1
        self.numeracion.append(0)
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        #self.numeracion[self.level] = 0
        self.numeracion.pop()
        self.level -= 1

    def print(self, text):
        self.numeracion[self.level] += 1
        numer = [str(i) for i in self.numeracion[:self.level+1]]
        print(f"{'  '*self.level}{'.'.join(numer)} {text}")

Usando la clase Indice, podemos generar el 铆ndice de un libro. La llamada a la funci贸n print del 铆ndice tendr谩 una funcionalidad distinta dependiendo de en que bloque se encuentre su llamada. Es decir, la funci贸n imprime algo diferente dependiendo del contexto en el que se est茅, entendiendo por contexto el n煤mero de bloques with que haya anidados.

with Indice() as indice:
    indice.print('Apartado')
    with indice:
        indice.print('Apartado')
        indice.print('Apartado')
        indice.print('Apartado')
        indice.print('Apartado')
        with indice:
            indice.print('Apartado')
            indice.print('Apartado')
            with indice:
                indice.print('Apartado')
                indice.print('Apartado')
    indice.print('Apartado')
    indice.print('Apartado')
    
# 1 Apartado
#   1.1 Apartado
#   1.2 Apartado
#   1.3 Apartado
#   1.4 Apartado
#     1.4.1 Apartado
#     1.4.2 Apartado
#       1.4.2.1 Apartado
#       1.4.2.2 Apartado
# 2 Apartado
# 3 Apartado