Administradores de contexto de Python

Uno de los más "oscuros" Las características de Python que casi todos los programadores de Python usan, incluso los principiantes, pero que realmente no entienden, es el contexto...

Introducción

Una de las características más "oscuras" de Python que casi todos los programadores de Python usan, incluso los principiantes, pero que realmente no entienden, son los administradores de contexto. Probablemente los haya visto en forma de declaraciones with, que generalmente se encuentran por primera vez cuando aprende a abrir archivos en Python. Aunque los administradores de contexto parecen un poco extraños al principio, cuando realmente nos sumergimos en ellos, entendemos la motivación y las técnicas detrás de ellos, tenemos acceso a una nueva arma en nuestro arsenal de programación. Entonces, sin más preámbulos, ¡vamos a sumergirnos en él!

Motivación: Gestión de recursos

Como decía alguien mucho más sabio que yo, "La necesidad es la madre de la invención". Para comprender realmente qué es un administrador de contexto y cómo podemos usarlo, primero debemos investigar las motivaciones detrás de él — las necesidades que dieron origen a esta "invención".

La principal motivación detrás de los administradores de contexto es la administración de recursos. Cuando un programa quiere obtener acceso a un recurso en la computadora, se lo solicita al sistema operativo y el sistema operativo, a su vez, le proporciona un identificador para ese recurso. Algunos ejemplos comunes de dichos recursos son archivos y puertos de red. Lo que es importante entender es que estos recursos tienen disponibilidad limitada, por ejemplo, un puerto de red puede ser utilizado por un solo proceso a la vez, y hay una cantidad limitada de puertos disponibles. Entonces, cada vez que abrimos un recurso, debemos recordar cerrarlo, para que el recurso se libere. Pero desafortunadamente, es más fácil decirlo que hacerlo.

La forma más sencilla de lograr una gestión adecuada de los recursos sería llamar a la función cerrar una vez que hayamos terminado con el recurso. Por ejemplo:

1
2
3
4
opened_file = open('readme.txt')
text = opened_file.read()
...
opened_file.close()

Aquí abrimos un archivo llamado readme.txt, leemos el archivo y guardamos su contenido en una cadena text, y luego, cuando terminamos, cerramos el archivo llamando al método close(). del objeto archivo_abierto. Ahora, a primera vista, esto puede parecer correcto, pero en realidad, no es nada sólido. Si ocurre algo inesperado entre la apertura y el cierre del archivo, lo que hace que el programa no pueda ejecutar la línea que contiene la instrucción close, se produciría una fuga de recursos. Estos eventos inesperados son lo que llamamos excepciones, uno común sería cuando alguien cierra el programa a la fuerza mientras se está ejecutando.

Ahora, la forma correcta de manejar esto sería usando Manejo de excepciones, usando bloques try...else. Mira el siguiente ejemplo:

1
2
3
4
5
6
try:
    opened_file = open('readme.txt')
    text = opened_file.read()
    ...
else:
    opened_file.close()

Python siempre se asegura de que se ejecute el código en el bloque else, independientemente de lo que pueda suceder. Esta es la forma en que los programadores en otros lenguajes manejarían la administración de recursos, pero los programadores de Python obtienen un mecanismo especial que les permite implementar la misma funcionalidad sin todo el repetitivo. Aquí es donde entran en juego los administradores de contexto.

Implementación de administradores de contexto

Ahora que hemos terminado con la parte más crucial sobre la comprensión de los administradores de contexto, podemos pasar a implementarlos. Para este tutorial, implementaremos una clase File personalizada. Es totalmente redundante ya que Python ya proporciona esto, pero sin embargo, será un buen ejercicio de aprendizaje ya que siempre podremos relacionarnos con la clase ‘Archivo’ que ya está en el estándar. biblioteca.

La forma estándar y de "nivel inferior" de implementar un administrador de contexto es definir dos métodos "mágicos" en la clase para la que desea implementar la administración de recursos, __enter__ y __exit__. Si te estás perdiendo—pensando, "¿qué es esta cosita del método mágico? Nunca he oído hablar de esto antes"—bueno, si has comenzado a hacer programación orientada a objetos en Python, seguramente ya has encontrado un método mágico, el método __init__.

A falta de mejores palabras, son métodos especiales que puede definir para hacer que sus clases sean más inteligentes o agregarles “magia”. Puedes encontrar una buena lista de referencia de todos los métodos mágicos disponibles en Python aquí.

De todos modos, volviendo al tema, antes de comenzar a implementar estos dos métodos mágicos, tendremos que entender su propósito. __enter__ es el método que se llama cuando abrimos el recurso, o para decirlo de una manera un poco más técnica — cuando "ingresamos" al contexto de tiempo de ejecución. La instrucción with vinculará el valor de retorno de este método al objetivo especificado en la cláusula as de la declaración.

Veamos un ejemplo:

1
2
3
4
5
6
7
class FileManager:
    def __init__(self, filename):
        self.filename = filename
        
    def __enter__(self):
        self.opened_file = open(self.filename)
        return self.opened_file

Como puede ver, el método __enter__ está abriendo el recurso—el archivo—y devolviéndolo. Cuando usamos este FileManager en una declaración with, se llamará a este método y su valor de retorno se vinculará a la variable de destino que mencionó en la cláusula as. Lo he demostrado en el siguiente fragmento de código:

1
2
with FileManager('readme.txt') as file:
    text = file.read()

Vamos a desglosarlo parte por parte. En primer lugar, se crea una instancia de la clase FileManager cuando la instanciamos, pasando el nombre de archivo "readme.txt" al constructor. Luego, la declaración with comienza a trabajar en él — llama al método __enter__ de ese objeto FileManager y asigna el valor devuelto a la variable file mencionada en la cláusula as. Luego, dentro del bloque with, podemos hacer lo que queramos con el recurso abierto.

La otra parte importante del rompecabezas es el método __exit__. El método __exit__ contiene un código de limpieza que debe ejecutarse una vez que hayamos terminado con el recurso, pase lo que pase. Las instrucciones en este método serán similares a las del bloque else que discutimos antes mientras discutíamos el manejo de excepciones. Para reiterar, el método __exit__ contiene instrucciones para cerrar correctamente el controlador de recursos, de modo que el recurso se libere para su uso posterior por parte de otros programas en el sistema operativo.

Ahora echemos un vistazo a cómo podríamos escribir este método:

1
2
3
class FileManager:
    def __exit__(self. *exc):
        self.opened_file.close()

Ahora, cada vez que las instancias de esta clase se usen en una sentencia with, se llamará a este método __exit__ antes de que el programa abandone el bloque with, o antes de que el programa se detenga debido a alguna excepción. Ahora veamos toda la clase FileManager para que tengamos una idea completa.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
class FileManager:
    def __init__(self, filename):
        self.filename = filename
        
    def __enter__(self):
        self.opened_file = open(self.filename)
        return self.opened_file
    
    def __exit__(self, *exc):
        self.opened_file.close()

Bastante simple, ¿verdad? Acabamos de definir las acciones de apertura y limpieza en los métodos mágicos respectivos, y Python se encargará de la administración de recursos donde sea que se use esta clase. Eso me lleva al siguiente tema, las diferentes formas en que podemos usar las clases de administrador de contexto, como esta clase FileManager.

Uso de administradores de contexto

No hay mucho que explicar aquí, así que en lugar de escribir párrafos largos, proporcionaré algunos fragmentos de código en esta sección:

1
2
3
4
file = FileManager('readme.txt')
with file as managed_file:
    text = managed_file.read()
    print(text)
1
2
3
with FileManager('readme.txt') as managed_file:
    text = managed_file.read()
    print(text)
1
2
3
4
5
6
7
def open_file(filename):
    file = FileManager(filename)
    return file

with open_file('readme.txt') as managed_file:
    text = managed_file.read()
    print(text)

Puedes ver que la clave para recordar es,

  1. El objeto pasado a la sentencia with debe tener los métodos __enter__ y __exit__.
  2. El método __enter__ debe devolver el recurso que se utilizará en el bloque with.

Importante: hay algunas sutilezas que omití para que la discusión fuera al grano. Para conocer las especificaciones exactas de estos métodos mágicos, consulte la documentación de Python aquí.

Usar contextlib

El Python's Zen—El principio rector de Python como una lista de aforismos—establece que,

Lo simple es mejor que lo complejo.

Para realmente llevar este punto a casa, los desarrolladores de Python han creado una biblioteca llamada contextlib que contiene utilidades relacionadas con los administradores de contexto, como si no simplificaran el problema de la gestión de recursos suficiente. Voy a demostrar solo uno de ellos brevemente aquí, te recomiendo que consultes los documentos oficiales de Python para obtener más información.

1
2
3
4
5
6
7
8
9
from contextlib import contextmanager

@contextmanager
def open_file(filename):
    opened_file = open(filename)
    try:
        yield opened_file
    finally:
        opened_file.close()

Al igual que el código anterior, podemos simplemente definir una función que ‘arroje’ el recurso protegido en una instrucción ‘intentar’, cerrándola en la siguiente instrucción ‘finalmente’. Otra forma de entenderlo:

  • Todo el contenido que de otro modo pondrías en el método __enter__, excepto la instrucción return, va antes del bloque try aquí — básicamente las instrucciones para abrir el recurso.
  • En lugar de devolver el recurso, lo ‘cedes’, dentro de un bloque ‘intentar’.
  • El contenido del método __exit__ va dentro del bloque finally correspondiente.

Una vez que tenemos dicha función, podemos decorarla usando el decorador contextlib.contextmanager y estamos bien.

1
2
3
with open_file('readme.txt') as managed_file:
    text = managed_file.read()
    print(text)

Como puede ver, la función open_file decorada devuelve un administrador de contexto y podemos usarlo directamente. Esto nos permite lograr el mismo efecto que al crear la clase FileManager, sin todas las molestias.

Lecturas adicionales {#lecturas adicionales}

Si te sientes entusiasmado y quieres leer más sobre los administradores de contexto, te animo a que consultes los siguientes enlaces:

Licensed under CC BY-NC-SA 4.0