Menu
Français

Décorateurs Python : @fonctions, arguments et functools.wraps

Ce que sont vraiment les décorateurs Python, comment écrire les tiens, et les motifs (arguments, empilement, wraps) qui les rendent utiles.

Un décorateur, c'est une fonction qui enveloppe une fonction

Cette phrase sonne abstraite, mais la mécanique est simple. Un décorateur prend une fonction en entrée, renvoie une fonction en sortie. La fonction qu'il renvoie appelle généralement l'originale, avec un comportement supplémentaire enveloppé autour de l'appel.

L'exemple le plus court possible :

main.py
Output
Click Run to see the output here.

shout est le décorateur. Il prend une fonction (greet), construit une nouvelle fonction (wrapper) qui appelle l'originale et met le résultat en majuscules, et la renvoie. Réaffecter greet = shout(greet) échange l'originale contre la version enveloppée.

Ce motif de réaffectation est tellement courant que Python lui a donné une syntaxe dédiée.

Le @ est du sucre pour une réaffectation

@name sur la ligne au-dessus d'un def équivaut à name = name(...) juste après la définition de la fonction :

main.py
Output
Click Run to see the output here.

@shout se lit « applique le décorateur shout à cette fonction ». Python exécute greet = shout(greet) immédiatement après le def — même mécanique qu'avant, moins de frappes.

Une fois que tu vois @name, remplace-le mentalement par function = name(function). C'est tout ce que la syntaxe veut dire.

Gérer les arguments

La plupart des fonctions prennent des arguments. Un décorateur utilisable les transmet. L'idiome est *args, **kwargs — la façon de Python d'accepter n'importe quels arguments — parce que le wrapper ne devrait pas se soucier de ce que la fonction enveloppée attend :

main.py
Output
Click Run to see the output here.

*args capture tous les arguments positionnels. **kwargs capture tous les arguments nommés. Le wrapper les transmet tous à la fonction enveloppée sans changement, puis fait le travail supplémentaire pour lequel le décorateur est fait — ici, mettre le résultat en majuscules.

C'est la forme que prennent la plupart des vrais décorateurs.

Un exemple plus utile : chronométrage

Afficher combien de temps prend une fonction :

main.py
Output
Click Run to see the output here.

Le motif — exécuter quelque chose avant l'appel, exécuter quelque chose après — est ce que la plupart des décorateurs finissent par faire. Logging, contrôles d'authentification, retries et validation d'entrée suivent tous la même forme.

Préserver l'identité originale : functools.wraps

Décorer une fonction la remplace, ce qui veut dire que la fonction enveloppée perd ses attributs __name__ et __doc__ originaux :

main.py
Output
Click Run to see the output here.

greet.__name__ est maintenant "wrapper" et la docstring a disparu. Ça casse help(), les traces de pile et tout outil qui inspecte la fonction.

La correction est une ligne : @functools.wraps(func) sur la fonction interne copie les métadonnées :

main.py
Output
Click Run to see the output here.

Ajoute toujours @wraps(func) à la fonction interne. Ça ne coûte rien et ça évite des sessions de débogage surprenantes plus tard.

Décorateurs avec arguments

Parfois le décorateur lui-même a besoin de configuration — « réessaie cette fonction jusqu'à 3 fois », « log au niveau DEBUG ». Ça veut dire une couche d'imbrication de plus : une fonction externe qui prend les arguments et renvoie un décorateur.

main.py
Output
Click Run to see the output here.

Trois couches, ça a l'air beaucoup. Lis de l'extérieur vers l'intérieur :

  1. repeat(times=3) est un appel de fonction. Il renvoie le decorator.
  2. decorator est le vrai décorateur — il prend une fonction et renvoie une enveloppée.
  3. wrapper est la fonction enveloppée qui s'exécute au moment de l'appel.

Cette forme alimente @retry(times=5), @cache(maxsize=100) et des décorateurs de framework comme @app.route("/users"). Une fois que tu vois le motif à trois couches, toute la famille se lit de la même façon.

Empiler les décorateurs

Tu peux appliquer plus d'un décorateur à une fonction. Ils s'empilent de bas en haut — celui le plus près du def s'exécute en premier :

main.py
Output
Click Run to see the output here.

add_exclaim enveloppe en premier, ajoutant le !. Puis shout enveloppe ça, mettant tout en majuscules. La sortie est HI, ROSA!.

L'ordre compte. Inverser la pile te donne HI, ROSA! avec le point d'exclamation ajouté après la mise en majuscules — visuellement identique ici, mais imagine un décorateur qui formate du JSON : l'exécuter avant ou après un décorateur qui log l'entrée peut produire des résultats très différents.

Décorateurs intégrés que tu verras

Python et sa bibliothèque standard sont livrés avec une poignée de décorateurs que tu croiseras dans du vrai code :

main.py
Output
Click Run to see the output here.
  • @property transforme une méthode en attribut calculé.
  • @staticmethod marque une méthode qui n'utilise pas self ni cls.
  • @classmethod reçoit la classe comme cls au lieu d'une instance — parfait pour des constructeurs alternatifs.
  • @functools.lru_cache mémoïse les résultats, donc les appels répétés avec les mêmes arguments touchent un cache.

Les décorateurs de framework (@app.route, @pytest.fixture, @dataclass) suivent la même mécanique. Rien de spécial — juste des fonctions qui enveloppent des fonctions.

Quand en écrire un, quand ne pas

Écris un décorateur quand tu veux appliquer le même comportement à plusieurs fonctions — chronométrage, logging, retries, contrôles d'autorisation. Tout l'intérêt, c'est que le comportement reste hors du corps de la fonction.

Saute le décorateur quand :

  • Le comportement appartient à une fonction spécifique. Mets-le dans la fonction.
  • Tu le veux seulement pour les tests. Une fixture ou un paramètre est plus clair.
  • Tu es tenté d'en empiler quatre ou cinq. À ce stade, le flux de contrôle est caché dans la chaîne de décorateurs — un lecteur doit démêler chaque couche pour voir ce qui s'exécute vraiment. Une simple fonction helper peut mieux se lire.

Les décorateurs sont un outil tranchant. Bien utilisés, ils gardent le code DRY et l'intention évidente. Mal utilisés, ils cachent ce que fait le programme. Penche vers « évident » quand tu décides d'en utiliser un.

Ensuite : les annotations de type

Les décorateurs sont un endroit courant pour rencontrer les annotations de type dans la nature — les fonctions wrapper annotent souvent leurs signatures. Les annotations de type sont une petite fonctionnalité qui paie vite, et elles arrivent ensuite.

Questions fréquentes

Qu'est-ce qu'un décorateur en Python ?

Un décorateur est une fonction qui prend une autre fonction et renvoie une nouvelle fonction — généralement une qui enveloppe l'originale avec un comportement supplémentaire. Tu en appliques un avec @decorator_name sur la ligne au-dessus d'un def. La syntaxe @ est un raccourci pour func = decorator_name(func).

À quoi servent les décorateurs en Python ?

Ajouter du comportement autour d'une fonction sans modifier son corps — logging, chronométrage, cache, contrôles d'authentification, validation d'entrée, retries. Les frameworks les utilisent beaucoup : @app.route(...) dans Flask, @pytest.fixture dans pytest, @property et @staticmethod intégrés.

Puis-je écrire mon propre décorateur ?

Oui. Un décorateur est juste une fonction qui prend une fonction et renvoie une fonction. La plupart des décorateurs personnalisés enveloppent l'appel original dans une petite fonction interne qui fait quelque chose avant, après ou autour. Utilise functools.wraps sur la fonction interne pour préserver le nom et la docstring de la fonction originale.

Apprendre à coder avec Coddy

COMMENCER