Cours de Python / Comprendre les décorateurs et les gestionnaires de contexte
Un décorateur peut envelopper l’appel d’une fonction, tandis qu’un gestionnaire de contexte organise une préparation et un nettoyage autour d’un bloc with. Pour les utiliser correctement, vérifiez ce qui arrive aux arguments, à la valeur renvoyée et aux exceptions, plutôt que de contrôler seulement un message affiché.
Distinguer l’enveloppe d’une fonction et la durée d’un contexte
La notation @announce placée avant une fonction n’est pas un commentaire : une fois la fonction créée, son nom est associé au résultat de announce(fonction). Dans l’exemple, ce résultat est une nouvelle fonction wrapper qui affiche un message, puis appelle la fonction d’origine.
Le décorateur functools.wraps conserve notamment le nom et la documentation de la fonction enveloppée. Cela facilite l’inspection et les outils, sans garantir automatiquement ses règles de type, de sécurité ou de gestion des erreurs. La référence de wraps explique cette conservation des métadonnées.
Le gestionnaire de contexte remplit un autre rôle : il délimite un bloc qui utilise une ressource ou un état temporaire. Ici, il fournit une liste puis la vide à la sortie. Les deux mécanismes peuvent se combiner sans devenir interchangeables : annoncer un appel ne nettoie pas une ressource, et nettoyer une ressource ne définit pas une politique de reprise.
Exécuter le décorateur et la collection temporaire
Créez un dossier d’exercice et enregistrez le fichier UTF-8 complet wrappers.py suivant. Il utilise uniquement la bibliothèque standard de Python :
from functools import wraps
from contextlib import contextmanager
def announce(func):
@wraps(func)
def wrapper(*args, **kwargs):
print("start")
return func(*args, **kwargs)
return wrapper
@contextmanager
def temporary_items():
items = []
try:
yield items
finally:
items.clear()
print("cleared")
@announce
def total(amounts):
return sum(amounts)
with temporary_items() as items:
items.extend([3500, 8000])
print(total(items))
print(total.__name__)
print(items)
Dans le terminal du dossier avec l’environnement choisi, lancez python wrappers.py. Sous macOS ou Ubuntu, utilisez python3 si ce nom correspond à votre interpréteur hors environnement virtuel. Le résultat attendu est :
start
11500
cleared
total
[]
start vient de l’enveloppe au moment où total est appelée. La fonction renvoie 11 500, que le programme affiche. À la sortie du with, le finally vide la liste et affiche cleared. Le nom affiché reste total grâce à wraps.
Le dernier [] ne signifie pas que le nom items a disparu : il désigne encore la liste, dont le contenu a été effacé par items.clear(). D’autres références à cette même liste constateraient aussi qu’elle est vide. L’instruction with ne crée pas, à elle seule, une portée de variables séparée.
Suivre la valeur renvoyée et la propagation des exceptions
Le générateur décoré avec @contextmanager doit produire exactement une valeur. Le code avant yield prépare le contexte ; yield items transmet cette liste au bloc ; à la sortie, l’exécution reprend dans le générateur. Si le bloc lève une exception, elle lui est transmise au niveau du yield, ce qui permet au finally d’assurer le nettoyage prévu. La référence de contextmanager détaille ce déroulement.
Un except autour du yield qui intercepte une erreur sans la relancer peut la supprimer du point de vue de l’appelant. Cette récupération doit être un choix explicite. Si vous voulez seulement noter l’échec ou nettoyer avant de le transmettre, conservez sa propagation, comme dans le cours sur les exceptions et finally.
Dans le décorateur, *args et **kwargs transmettent les arguments positionnels et nommés. Le mot return transmet le résultat : si vous le retirez de return func(*args, **kwargs), la fonction d’origine s’exécute encore, mais l’enveloppe renvoie implicitement None. Préserver le nom avec wraps ne corrige pas cette modification du comportement.
Le décorateur announce ne capture pas les exceptions de la fonction appelée : elles remontent à l’appelant. Il n’ajoute pas non plus de validation des montants. Définissez ces contrats dans les fonctions concernées et contrôlez-les après l’ajout d’une enveloppe.
Provoquer les échecs puis choisir la bonne abstraction
- Dans une copie du programme, remplacez seulement
print(total(items))parraise ValueError("planned failure"). Le nettoyage doit affichercleared, puis l’exception doit rester visible. Les instructions placées après le bloc ne sont pas exécutées normalement dans ce cas. - Pour examiner la liste après cet échec, entourez le bloc
withd’untryet interceptez cetteValueErrorà l’extérieur. Vous pouvez alors vérifier queitems == []. Ne faites pas disparaître les erreurs de votre application réelle uniquement pour reproduire cet affichage. - Rétablissez le programme, puis retirez temporairement le seul
returnde l’enveloppe. ComparezNoneau total attendu. Remettez ensuitereturnet confirmez les cinq lignes du résultat initial.
Appelez temporary_items() pour créer un nouveau contexte à chaque utilisation ; ne réutilisez pas une instance de gestionnaire fondée sur ce générateur après sa sortie. Distinguez également le nettoyage local d’une annulation de toutes les actions déjà réalisées : vider une liste ne revient pas sur une requête distante ou sur un fichier modifié.
Un décorateur de reprise peut répéter une écriture et créer plusieurs opérations si celle-ci n’est pas conçue pour être rejouée. Choisissez donc séparément la gestion des ressources et les règles de reprise. Pour un traitement court exécuté une seule fois, une structure directe peut rester plus lisible qu’une abstraction supplémentaire ; l’intérêt vient du comportement clarifié et réutilisable, pas du seul ajout de symboles @.




