Les gestionnaires de contexte permettent à Python de garantir qu’une ressource sera nettoyée, que le code du bloc réussisse ou qu’il échoue. Un article technique détaille leur fonctionnement sous l’instruction with, ainsi que plusieurs façons de les construire et de les utiliser, bien au-delà de la gestion des fichiers.
- Un gestionnaire de contexte garantit le nettoyage d’une ressource, même en cas d’erreur.
- Une valeur vraie renvoyée par __exit__() supprime l’exception sans trace.
- Une classe conserve un état lisible, tandis que @contextmanager suffit pour une tâche linéaire.
- La bibliothèque standard et ExitStack gèrent les répertoires temporaires, les dictionnaires et un nombre variable de ressources.
Beaucoup de développeurs découvrent ce mécanisme avec with open(path), qui ferme le fichier automatiquement. Selon l’auteur, les fichiers sont pourtant le cas le moins intéressant : l’instruction with délimite surtout un bloc dont la sortie est garantie. Le protocole repose sur deux méthodes, __enter__(), appelée à l’entrée du bloc, et __exit__(), appelée à la sortie, qu’elle soit normale, causée par un return ou due à une exception.
La valeur renvoyée par __exit__() joue un rôle décisif. Si elle est vraie, l’exception est supprimée, ce qui peut passer inaperçu : le programme continue, et l’erreur disparaît sans trace. Si elle est False ou absente, l’exception remonte après le nettoyage. C’est pourquoi les gestionnaires présentés dans l’article ne renvoient jamais de valeur vraie.
Construire ses propres gestionnaires
Une classe convient lorsque le gestionnaire doit conserver un état que l’appelant peut relire. L’article prend l’exemple d’un minuteur, qui mesure la durée d’un bloc grâce à time.perf_counter() et affiche le résultat même si le bloc échoue. Une extraction de caractéristiques interrompue affiche ainsi « feature extraction: 12.418s (failed) », avant que l’erreur ne remonte.
Pour une tâche linéaire, c’est-à-dire une mise en place, un bloc puis une restauration, le décorateur @contextmanager permet d’écrire une simple fonction génératrice. Tout ce qui précède le yield s’exécute à l’entrée, tout ce qui suit à la sortie. L’article recommande de placer la restauration dans un finally, et de ne jamais avaler l’exception dans un try/except sans la relancer. Une seule valeur doit être produite par le yield, faute de quoi une erreur RuntimeError est levée.
Restaurer l’état et gérer plusieurs ressources
La bibliothèque standard fournit déjà plusieurs gestionnaires. TemporaryDirectory() crée un répertoire au nom unique et le supprime à la fin du bloc, ce qui évite les fichiers résiduels d’un exécution précédente. Depuis Python 3.12, le paramètre delete=False permet de conserver les fichiers pour une analyse a posteriori. Sous Windows, un fichier resté ouvert peut faire échouer la suppression ; le paramètre ignore_cleanup_errors=True, disponible depuis Python 3.10, atténue ce problème sans le résoudre.
Pour les modifications de dictionnaires, patch.dict(), issu du module unittest.mock, restaure l’état d’origine après le bloc : une clé existante reprend sa valeur, et une clé absente est supprimée. L’article souligne toutefois qu’os.environ est global au processus. Toutes les tâches et tous les threads voient alors la modification, ce qui la rend risquée en code concurrent.
Lorsque le nombre de ressources n’est connu qu’à l’exécution, par exemple le nombre de fichiers à fusionner, ExitStack regroupe les gestionnaires. Sa méthode enter_context() enregistre un gestionnaire, et callback() une simple fonction. À la sortie, les éléments sont dépilés dans l’ordre inverse. L’article précise que ce dépilement intervient à la fin du bloc ou lors d’un appel à close(), et non à la suppression de l’objet par le ramasse-miettes. Une classe dédiée peut être préférable lorsque la pile devient trop lourde.
