Los gestores de contexto permiten a Python garantizar que un recurso será limpiado, ya sea que el código del bloque tenga éxito o falle. Un artículo técnico detalla su funcionamiento bajo la instrucción with, así como varias formas de construirlos y usarlos, mucho más allá de la gestión de archivos.
- Los gestores de contexto garantizan la limpieza de un recurso, tanto si el bloque tiene éxito como si falla.
- Si __exit__() devuelve True, la excepción se suprime en silencio, por eso no debe hacerlo.
- Una clase sirve para conservar estado, y @contextmanager basta para tareas lineales.
- ExitStack gestiona un número variable de recursos y los libera en orden inverso al salir.
Muchos desarrolladores descubren este mecanismo con with open(path), que cierra el archivo automáticamente. Según el autor, los archivos son sin embargo el caso menos interesante: la instrucción with delimita sobre todo un bloque cuya salida está garantizada. El protocolo se basa en dos métodos, __enter__(), llamado al entrar al bloque, y __exit__(), llamado al salir, ya sea normalmente, causado por un return o debido a una excepción.
El valor devuelto por __exit__() juega un papel decisivo. Si es verdadero, la excepción se suprime, lo que puede pasar desapercibido: el programa continúa y el error desaparece sin dejar rastro. Si es False o está ausente, la excepción se propaga después de la limpieza. Por eso los gestores presentados en el artículo nunca devuelven un valor verdadero.
Construir tus propios gestores
Una clase es adecuada cuando el gestor debe conservar un estado que el llamador pueda leer. El artículo pone el ejemplo de un temporizador, que mide la duración de un bloque gracias a time.perf_counter() y muestra el resultado incluso si el bloque falla. Una extracción de características interrumpida muestra así « feature extraction: 12.418s (failed) », antes de que el error se propague.
Para una tarea lineal, es decir, una preparación, un bloque y luego una restauración, el decorador @contextmanager permite escribir una simple función generadora. Todo lo que precede al yield se ejecuta al entrar, todo lo que sigue al salir. El artículo recomienda colocar la restauración en un finally, y nunca tragar la excepción en un try/except sin relanzarla. Solo debe producirse un valor por el yield, de lo contrario se lanza un error RuntimeError.
Restaurar el estado y gestionar varios recursos
La biblioteca estándar ya proporciona varios gestores. TemporaryDirectory() crea un directorio con un nombre único y lo elimina al final del bloque, lo que evita archivos residuales de una ejecución previa. Desde Python 3.12, el parámetro delete=False permite conservar los archivos para un análisis posterior. En Windows, un archivo abierto puede impedir la eliminación; el parámetro ignore_cleanup_errors=True, disponible desde Python 3.10, atenúa este problema sin resolverlo.
Para modificaciones de diccionarios, patch.dict(), del módulo unittest.mock, restaura el estado original después del bloque: una clave existente recupera su valor, y una clave ausente se elimina. Sin embargo, el artículo señala que os.environ es global al proceso. Todas las tareas y hilos ven entonces la modificación, lo que la hace arriesgada en código concurrente.
Cuando el número de recursos solo se conoce en tiempo de ejecución, por ejemplo el número de archivos a fusionar, ExitStack agrupa los gestores. Su método enter_context() registra un gestor, y callback() una función simple. Al salir, los elementos se desapilan en orden inverso. El artículo precisa que este desapilado ocurre al final del bloque o al llamar a close(), y no al eliminar el objeto por el recolector de basura. Una clase dedicada puede ser preferible cuando la pila se vuelve demasiado pesada.
