• Aucun résultat trouvé

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

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 !")

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.

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

"""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():

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(...): ...

Documents relatifs