Une fois n'est pas coutume, je vais vous montrer les différentes constructions possibles en théorie avec quelques exemples, mais je vais aussi consacrer une section entière à des exemples d'utilisations pour expliciter cette partie théorique indispensable.
Format le plus simple
Comme je l'ai dit, les décorateurs sont des fonctions « classiques » de Python, dans leur définition. Ils ont une petite subtilité en ce qu'ils prennent en paramètre une fonction et renvoient une fonction.
On déclare qu'une fonction doit être modifiée par un (ou plusieurs) décorateurs grâce à une (ou plusieurs) lignes au-dessus de la définition de fonction, comme ceci :
Code : Python | @nom_du_decorateur
def ma_fonction(...)
|
Le décorateur s'exécute au moment de la définition de fonction et non lors de l'appel. Ceci est important. Il prend en paramètre, comme je l'ai dit, une fonction (celle qu'il modifie) et renvoie une fonction (qui peut être la même).
Voyez plutôt :
Code : Python Console 1
2
3
4
5
6
7
8
9
10
11
12 | >>> def mon_decorateur(fonction):
... """Premier exemple de décorateur"""
... print("Notre décorateur est appelé avec en paramètre la fonction {0}".format(fonction))
... return fonction
...
>>> @mon_decorateur
... def salut():
... """Fonction modifiée par notre décorateur"""
... print("Salut !")
...
Notre décorateur est appelé avec en paramètre la fonction <function salut at 0x00BA5198>
>>>
|
Euh… qu'est-ce qu'on a fait là ?
- D'abord, on crée le décorateur. Il prend en paramètre, comme je vous l'ai dit, la fonction qu'il modifie. Dans notre exemple, il se contente d'afficher cette fonction puis de la renvoyer.
- On crée ensuite la fonction salut. Comme vous le voyez, on indique avant la définition la ligne @mon_decorateur, qui précise à Python que cette fonction doit être modifiée par notre décorateur. Notre fonction est très utile : elle affiche « Salut ! » et c'est tout.
- À la fin de la définition de notre fonction, on peut voir que le décorateur est appelé. Si vous regardez plus attentivement la ligne affichée, vous vous rendez compte qu'il est appelé avec, en paramètre, la fonction salut que nous venons de définir.
Intéressons-nous un peu plus à la structure de notre décorateur. Il prend en paramètre la fonction à modifier (celle que l'on définit sous la ligne du
@), je pense que vous avez pu le constater. Mais il renvoie également cette fonction et cela, c'est un peu moins évident !
En fait, la fonction renvoyée remplace la fonction définie. Ici, on renvoie la fonction définie, c'est donc la même. Mais on peut demander à Python d'exécuter une autre fonction à la place, pour modifier son comportement. Nous allons voir cela un peu plus loin.
Pour l'heure, souvenez-vous que les deux codes ci-dessous sont identiques :
Code : Python | # Exemple avec décorateur
@decorateur
def fonction(...):
...
|
Code : Python | # Exemple équivalent, sans décorateur
def fonction(...):
...
fonction = decorateur(fonction)
|
Relisez bien ces deux codes, ils font la même chose. Le second est là pour que vous compreniez ce que fait Python quand il manipule des fonctions modifiées par un (ou plusieurs) décorateur(s).
Quand vous exécutez
salut, vous ne voyez aucun changement. Et c'est normal puisque nous renvoyons la même fonction. Le seul moment où notre décorateur est appelé, c'est lors de la définition de notre fonction. Notre fonction
salut n'a pas été modifiée par notre décorateur, on s'est contenté de la renvoyer telle quelle.
Modifier le comportement de notre fonction
Vous l'aurez deviné, un décorateur comme nous l'avons créé plus haut n'est pas bien utile. Les décorateurs servent surtout à modifier le comportement d'une fonction. Je vous montre cependant pas à pas comment cela fonctionne, sinon vous risquez de vite vous perdre.
Comment faire pour modifier le comportement de notre fonction ?
En fait, vous avez un élément de réponse un peu plus haut. J'ai dit que notre décorateur prenait en paramètre la fonction définie et renvoyait une fonction (peut-être la même, peut-être une autre). C'est cette fonction renvoyée qui sera directement affectée à notre fonction définie. Si vous aviez renvoyé une autre fonction que
salut, dans notre exemple ci-dessus, la fonction
salut aurait redirigé vers cette fonction renvoyée.
Mais alors… il faut définir encore une fonction ?
Eh oui ! Je vous avais prévenus (et ce n'est que le début), notre construction se complexifie au fur et à mesure : on va devoir créer une nouvelle fonction qui sera chargée de modifier le comportement de la fonction définie. Et, parce que notre décorateur sera le seul à utiliser cette fonction, on va la définir directement dans le corps de notre décorateur.
Je suis perdu. Comment cela marche-t-il, concrètement ?
Je vais vous mettre le code, cela vaudra mieux que des tonnes d'explications. Je le commente un peu plus bas, ne vous inquiétez pas :
Code : Python 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 | def mon_decorateur(fonction):
"""Notre décorateur : il va afficher un message avant l'appel de la
fonction définie"""
def fonction_modifiee():
"""Fonction que l'on va renvoyer. Il s'agit en fait d'une version
un peu modifiée de notre fonction originellement définie. On se
contente d'afficher un avertissement avant d'exécuter notre fonction
originellement définie"""
print("Attention ! On appelle {0}".format(fonction))
return fonction()
return fonction_modifiee
@mon_decorateur
def salut():
print("Salut !")
|
Voyons l'effet, avant les explications. Aucun message ne s'affiche en exécutant ce code. Par contre, si vous exécutez votre fonction
salut :
Code : Python Console | >>> salut()
Attention ! On appelle <function salut at 0x00BA54F8>
Salut !
>>>
|
Et si vous affichez la fonction
salut dans l'interpréteur, vous obtenez quelque chose de surprenant :
Code : Python Console | >>> salut
<function fonction_modifiee at 0x00BA54B0>
>>>
|
Pour comprendre, revenons sur le code de notre décorateur :
- Comme toujours, il prend en paramètre une fonction. Cette fonction, quand on place l'appel au décorateur au-dessus de def salut, c'est salut (la fonction définie à l'origine).
- Dans le corps même de notre décorateur, vous pouvez voir qu'on a défini une nouvelle fonction, fonction_modifiee. Elle ne prend aucun paramètre, elle n'en a pas besoin. Dans son corps, on affiche une ligne avertissant qu'on va exécuter la fonction fonction (là encore, il s'agit de salut). À la ligne suivante, on l'exécute effectivement et on renvoie le résultat de son exécution (dans le cas de salut, il n'y en a pas mais d'autres fonctions pourraient renvoyer des informations).
- De retour dans notre décorateur, on indique qu'il faut renvoyer fonction_modifiee.
Lors de la définition de notre fonction
salut, on appelle notre décorateur. Python lui passe en paramètre la fonction
salut. Cette fois, notre décorateur ne renvoie pas
salut mais
fonction_modifiee. Et notre fonction
salut, que nous venons de définir, sera donc remplacée par notre fonction
fonction_modifiee, définie dans notre décorateur.
Vous le voyez bien, d'ailleurs : quand on cherche à afficher
salut dans l'interpréteur, on obtient
fonction_modifiee.
Souvenez-vous bien que le code :
Code : Python | @mon_decorateur
def salut():
...
|
revient au même, pour Python, que le code :
Code : Python | def salut():
...
salut = mon_decorateur(salut)
|
Ce n'est peut-être pas plus clair. Prenez le temps de lire et de bien comprendre l'exemple. Ce n'est pas simple, la logique est bel et bien là mais il faut passer un certain temps à tester avant de bien intégrer cette notion.
Pour résumer, notre décorateur renvoie une fonction de substitution. Quand on appelle
salut, on appelle en fait notre fonction modifiée qui appelle également
salut après avoir affiché un petit message d'avertissement.
Autre exemple : un décorateur chargé tout simplement d'empêcher l'exécution de la fonction. Au lieu d'exécuter la fonction d'origine, on lève une exception pour avertir l'utilisateur qu'il utilise une fonctionnalité obsolète.
Code : Python | def obsolete(fonction_origine):
"""Décorateur levant une exception pour noter que la fonction_origine
est obsolète"""
def fonction_modifiee():
raise RuntimeError("la fonction {0} est obsolète !".format(fonction_origine))
return fonction_modifiee
|
Là encore, faites quelques essais : tout deviendra limpide après quelques manipulations.
Un décorateur avec des paramètres
Toujours plus dur ! On voudrait maintenant passer des paramètres à notre décorateur. Nous allons essayer de coder un décorateur chargé d'exécuter une fonction en contrôlant le temps qu'elle met à s'exécuter. Si elle met un temps supérieur à la durée passée en paramètre du décorateur, on affiche une alerte.
La ligne appelant notre décorateur, au-dessus de la définition de notre fonction, sera donc sous la forme :
Code : Python | @controler_temps(2.5) # 2,5 secondes maximum pour la fonction ci-dessous
|
Jusqu'ici, nos décorateurs ne comportaient aucune parenthèse après leur appel. Ces deux parenthèses sont très importantes : notre fonction de décorateur prendra en paramètres non pas une fonction, mais les paramètres du décorateur (ici, le temps maximum autorisé pour la fonction). Elle ne renverra pas une fonction de substitution, mais un décorateur.
Encore et toujours perdu. Pourquoi est-ce si compliqué de passer des paramètres à notre décorateur ?
En fait… ce n'est pas si compliqué que cela mais c'est dur à saisir au début. Pour mieux comprendre, essayez encore une fois de vous souvenir que ces deux codes reviennent au même :
Code : Python | @decorateur
def fonction(...):
...
|
Code : Python | def fonction(...):
...
fonction = decorateur(fonction)
|
C'est la dernière ligne du second exemple que vous devez retenir et essayer de comprendre :
fonction = decorateur(fonction).
On remplace la fonction que nous avons définie au-dessus par la fonction que renvoie notre décorateur.
C'est le mécanisme qui se cache derrière notre
@decorateur.
Maintenant, si notre décorateur attend des paramètres, on se retrouve avec une ligne comme celle-ci :
Code : Python | @decorateur(parametre)
def fonction(...):
...
|
Et si vous avez compris l'exemple ci-dessus, ce code revient au même que :
Code : Python | def fonction(...):
...
fonction = decorateur(parametre)(fonction)
|
Je vous avais prévenus, ce n'est pas très intuitif ! Mais relisez bien ces exemples, le déclic devrait se faire tôt ou tard.
Comme vous le voyez, on doit définir comme décorateur une fonction qui prend en arguments les paramètres du décorateur (ici, le temps attendu) et qui renvoie un décorateur. Autrement dit, on se retrouve encore une fois avec un niveau supplémentaire dans notre fonction.
Je vous donne le code sans trop insister. Si vous arrivez à comprendre la logique qui se trouve derrière, c'est tant mieux, sinon n'hésitez pas à y revenir plus tard :
Code : Python 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30 | """Pour gérer le temps, on importe le module time
On va utiliser surtout la fonction time() de ce module qui renvoie le nombre
de secondes écoulées depuis le premier janvier 1970 (habituellement).
On va s'en servir pour calculer le temps mis par notre fonction pour
s'exécuter"""
import time
def controler_temps(nb_secs):
"""Contrôle le temps mis par une fonction pour s'exécuter.
Si le temps d'exécution est supérieur à nb_secs, on affiche une alerte"""
def decorateur(fonction_a_executer):
"""Notre décorateur. C'est lui qui est appelé directement LORS
DE LA DEFINITION de notre fonction (fonction_a_executer)"""
def fonction_modifiee():
"""Fonction renvoyée par notre décorateur. Elle se charge
de calculer le temps mis par la fonction à s'exécuter"""
tps_avant = time.time() # Avant d'exécuter la fonction
valeur_renvoyee = fonction_a_executer() # On exécute la fonction
tps_apres = time.time()
tps_execution = tps_apres - tps_avant
if tps_execution >= nb_secs:
print("La fonction {0} a mis {1} pour s'exécuter".format( \
fonction_a_executer, tps_execution))
return valeur_renvoyee
return fonction_modifiee
return decorateur
|
Ouf ! Trois niveaux dans notre fonction ! D'abord
controler_temps, qui définit dans son corps notre décorateur
decorateur, qui définit lui-même dans son corps notre fonction modifiée
fonction_modifiee.
J'espère que vous n'êtes pas trop embrouillés. Je le répète, il s'agit d'une fonctionnalité très puissante mais qui n'est pas très intuitive quand on n'y est pas habitué. Jetez un coup d'œil du côté des exemples au-dessus si vous êtes un peu perdus.
Nous pouvons maintenant utiliser notre décorateur. J'ai fait une petite fonction pour tester qu'un message s'affiche bien si notre fonction met du temps à s'exécuter. Voyez plutôt :
Code : Python Console | >>> @controler_temps(4)
... def attendre():
... input("Appuyez sur Entrée...")
...
>>> attendre() # Je vais appuyer sur Entrée presque tout de suite
Appuyez sur Entrée...
>>> attendre() # Cette fois, j'attends plus longtemps
Appuyez sur Entrée...
La fonction <function attendre at 0x00BA5810> a mis 4.14100003242 pour s'exécuter
>>>
|
Ça marche ! Et même si vous devez passer un peu de temps sur votre décorateur, vu ses différents niveaux, vous êtes obligés de reconnaître qu'il s'utilise assez simplement.
Il est quand même plus intuitif d'écrire :
Code : Python | @controler_temps(4)
def attendre(...)
...
|
que :
Code : Python | def attendre(...):
...
attendre = controler_temps(4)(attendre)
|
Tenir compte des paramètres de notre fonction
Jusqu'ici, nous n'avons travaillé qu'avec des fonctions ne prenant aucun paramètre. C'est pourquoi notre fonction
fonction_modifiee n'en prenait pas non plus.
Oui mais… tenir compte des paramètres, cela peut être utile. Sans quoi on ne pourrait construire que des décorateurs s'appliquant à des fonctions sans paramètre.
Il faut, pour tenir compte des paramètres de la fonction, modifier ceux de notre fonction
fonction_modifiee. Là encore, je vous invite à regarder les exemples ci-dessus, explicitant ce que Python fait réellement lorsqu'on définit un décorateur avant une fonction. Vous pourrez vous rendre compte que
fonction_modifiee remplace notre fonction et que, par conséquent, elle doit prendre des paramètres si notre fonction définie prend également des paramètres.
C'est dans ce cas en particulier que nous allons pouvoir réutiliser la notation spéciale pour nos fonctions attendant un nombre variable d'arguments. En effet, le décorateur que nous avons créé un peu plus haut devrait pouvoir s'appliquer à des fonctions ne prenant aucun paramètre, ou en prenant un, ou plusieurs… au fond, notre décorateur ne doit ni savoir combien de paramètres sont fournis à notre fonction, ni même s'en soucier.
Là encore, je vous donne le code adapté de notre fonction modifiée. Souvenez-vous qu'elle est définie dans notre
decorateur, lui-même défini dans
controler_temps (je ne vous remets que le code de
fonction_modifiee).
Code : Python 1
2
3
4
5
6
7
8
9
10
11
12
13 | ...
def fonction_modifiee(*parametres_non_nommes, **parametres_nommes):
"""Fonction renvoyée par notre décorateur. Elle se charge
de calculer le temps mis par la fonction à s'exécuter"""
tps_avant = time.time() # avant d'exécuter la fonction
ret = fonction_a_executer(*parametres_non_nommes, **parametres_nommes)
tps_apres = time.time()
tps_execution = tps_apres - tps_avant
if tps_execution >= nb_secs:
print("La fonction {0} a mis {1} pour s'exécuter".format( \
fonction_a_executer, tps_execution))
return ret
|
À présent, vous pouvez appliquer ce décorateur à des fonctions ne prenant aucun paramètre, ou en prenant un certain nombre, nommés ou non. Pratique, non ?
Des décorateurs s'appliquant aux définitions de classes
Vous pouvez également appliquer des décorateurs aux définitions de classes. Nous verrons un exemple d'application dans la section suivante. Au lieu de recevoir en paramètre la fonction, vous allez recevoir la classe.
Code : Python Console | >>> def decorateur(classe):
... print("Définition de la classe {0}".format(classe))
... return classe
...
>>> @decorateur
... class Test:
... pass
...
Définition de la classe <class '__main__.Test'>
>>>
|
Voilà. Vous verrez dans la section suivante quel peut être l'intérêt de manipuler nos définitions de classes à travers des décorateurs. Il existe d'autres exemples que celui que je vais vous montrer, bien entendu.
Chaîner nos décorateurs
Vous pouvez modifier une fonction ou une définition de classe par le biais de plusieurs décorateurs, sous la forme :
Code : Python | @decorateur1
@decorateur2
def fonction():
|
Ce n'est pas plus compliqué que ce que vous venez de faire. Je vous le montre pour qu'il ne subsiste aucun doute dans votre esprit, vous pouvez tester à loisir cette possibilité, par vous-mêmes.
Je vais à présent vous présenter quelques applications possibles des décorateurs, inspirées en grande partie de
la PEP 318.