Les décorateurs¶
Un décorateur est une fonction qui prend en paramètre une fonction et qui retourne une fonction. Ce type particulier de fonctions est également appelé une fonction d’ordre supérieur (higher order function). L’intérêt d’un décorateur est qu’il permet de transformer le comportement de la fonction passée en paramètre pour exécuter des traitements supplémentaires avant ou après les traitement normal de la fonction passée en paramètre.
Les décorateurs ont des usages multiples. Ils permettent de :
vérifier des conditions particulières lors de l’appel à une fonction
acquérir une ressource le temps de l’appel à la fonction (ouverture de fichier, accès à un service distant de base de données…)
tracer les appels de fonctions pour des raisons de débogage.
…
Nous verrons également qu’ils sont très utiles pour la programmation orientée objet.
Un premier décorateur¶
Prenons l’exemple d’un décorateur qui se contente d’écrire sur la sortie standard le début de l’appel à une fonction et la fin de l’appel :
def trace(func):
def decorateur():
print("Début d'appel à", func)
func()
print("Fin d'appel à", func)
return decorateur
Il est possible maintenant de décorer un appel à une fonction :
1 2 3 4 5 6 7 8 9 10 | def do_something():
print("doing something")
do_something = trace(do_something)
do_something()
# Affiche
# Début d'appel à <function do_something at 0x7f210f642730>
# doing something
# Fin d'appel à <function do_something at 0x7f210f642730>
|
À la ligne 4, on décore la fonction do_something()
en créant une nouvelle
fonction que nous affectons directement à do_something
pour changer
le comportement initial de la fonction.
Les décorateurs agissent comme un composé de fonctions en mathématiques. Pour
deux fonctions g(x)
et f(x)
, cela équivaut à :
g O f(x)
En Python, le pie operator @
permet de réaliser la même opération de manière
beaucoup plus simple :
1 2 3 4 5 6 7 8 9 | @trace
def do_something():
print("doing something")
do_something()
# Affiche
# Début d'appel à <function do_something at 0x7f210f642730>
# doing something
# Fin d'appel à <function do_something at 0x7f210f642730>
|
À la ligne 1, on décore la fonction do_something grâce à la notation @trace
Décorer des fonctions avec des paramètres et des valeurs de retour¶
Un décorateur est une fonction qui appelle la fonction décorée. Si cette dernière attend des paramètres et/ou retourne des valeurs, alors le décorateur doit en tenir compte :
def trace(func):
def decorateur(*args, **kwargs):
print("Début d'appel à", func)
resultat = func(*args, **kwargs)
print("Fin d'appel à", func)
return resultat
return decorateur
@trace
def moyenne(x, *args):
"""Calcule la moyenne d'un nombre quelconque de valeurs passées en paramètres."""
nb = 1 + len(args)
somme = x
for y in args:
somme += y
return somme / nb
resultat = moyenne(5, 2, 2)
# Affiche
# Début d'appel à <function moyenne at 0x7f210a4a7a60>
# Fin d'appel à <function moyenne at 0x7f210a4a7a60>
print("La moyenne est", resultat)
# Affiche La moyenne est 3.0
Comme un décorateur est une simple fonction, il est possible de décorer des fonctions
même sans utiliser le pie operator @
. On peut donc décorer des méthodes
natives de Python comme max()
max = trace(max)
resultat = max(1, 2, 3)
# Affiche
# Début d'appel à <built-in function max>
# Fin d'appel à <built-in function max>
print("Le maximum est", resultat)
# Affiche Le maximum est 3
Décorateur avec des paramètres¶
Il est possible de créer des décorateurs qui acceptent eux-mêmes des paramètres. En fait il suffit de créer une fonction qui retourne un décorateur. Ce dernier devant à son tour retourner la fonction de décoration.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | def repeter(nb_fois):
def repeter_decorateur(func):
def decorateur(*args, **kwargs):
for i in range(nb_fois):
func(*args, **kwargs)
return decorateur
return repeter_decorateur
@repeter(3)
def do_something():
print("do something")
do_something()
# Affiche
# do something
# do something
# do something
|
Conserver la documentation malgré l’utilisation d’un décorateur¶
L’utilisation d’un décorateur n’est pas totalement transparente. En effet, le décorateur produit généralement une fonction qui replace la fonction d’origine. Reprenons l’exemple précédent :
def trace(func):
def decorateur(*args, **kwargs):
print("Début d'appel à", func)
resultat = func(*args, **kwargs)
print("Fin d'appel à", func)
return resultat
return decorateur
@trace
def moyenne(x, *args):
"""Calcule la moyenne d'un nombre quelconque de valeurs passées en paramètres."""
nb = 1 + len(args)
somme = x
for y in args:
somme += y
return somme / nb
Si nous voulons afficher la document de la fonction moyenne()
:
help(moyenne)
Nous verrons s’afficher :
Help on function decorateur in module __main__:
decorateur(*args, **kwargs)
En effet, la méthode moyenne()
a été décorée. C’est donc la documentation
de la méthode interne decorateur(*args, **kwargs)
qui s’affiche et nous
avons perdu la véritable documentation d’origine de la fonction moyenne()
.
Le module functools
fournit un certain nombre de décorateur dont
functools.wraps()
qui permet de corriger ce problème :
from functools import wraps
def trace(func):
@wraps(func)
def decorateur(*args, **kwargs):
print("Début d'appel à", func)
resultat = func(*args, **kwargs)
print("Fin d'appel à", func)
return resultat
return decorateur
Maintenant nous pouvons utiliser ce décorateur et la documentation de la méthode décorée est conservée :
@trace
def moyenne(x, *args):
"""Calcule la moyenne d'un nombre quelconque de valeurs passées en paramètres."""
nb = 1 + len(args)
somme = x
for y in args:
somme += y
return somme / nb
help(moyenne)
# Affiche
# moyenne(x, *args)
# Calcule la moyenne d'un nombre quelconque de valeurs passées en paramètres.
Appliquer plusieurs décorateurs¶
Il est possible d’ajouter plusieurs décorateurs sur une fonction :
@decorateur1
@decorateur2
def do_something():
pass
Cela est équivalent à réaliser les appels suivants :
do_something = decorateur1(decorateur2(do_something))
L’ordre de déclaration des décorateurs est important puisqu’ils seront appliqués dans cet ordre.