XXIV. Les décorateurs▲
XXIV-1. Préambule▲
Le décorateur est un mécanisme permettant d’encapsuler une fonction quelconque dans une surcouche de traitements qui pourront s’exécuter avant ou après chaque appel à la fonction.
Cela permet de rajouter et enlever facilement des mécanismes d'optimisations (log, compteur d'appels, mise en mémoire des résultats les plus demandés) sur des fonctions du programme y compris sur des fonctions provenant d'une librairie externe et sur lesquelles on n’a aucune possibilité de modification.
Le principe du décorateur s’appuie sur le fait qu’une fonction, comme tout en Python, est avant tout un objet manipulable.
On peut donc par exemple l’assigner à une variable. Notez que dans ce cas, il n’y a pas de parenthèses au nom de la fonction, car on demande à récupérer l’objet fonction et non le résultat de son exécution.
2.
3.
4.
5.
6.
7.
8.
9.
10.
>>>
def
bonjour
(
s, n=
1
):
... print
(
"[
%s
]"
%
s *
n)
...
>>>
salut=
bonjour # Pas de parenthèses après le nom de fonction !
>>>
>>>
bonjour
(
"hello"
, 5
) # On peut donc appeler la fonction…
[hello][hello][hello][hello][hello]
>>>
>>>
salut
(
"hello "
, 5
) # … tout comme on peut appeler sa copie
[hello][hello][hello][hello][hello]
On peut même supprimer la fonction d’origine vu qu’elle est référencée ailleurs, son bloc d’instructions ne sera pas supprimé.
2.
3.
4.
5.
6.
7.
8.
9.
>>>
del
bonjour
>>>
>>>
bonjour
(
"hello "
, 5
)
Traceback (
most recent call last):
File "<stdin>"
, line 1
, in
<
module>
NameError
: name 'bonjour'
is
not
defined
>>>
>>>
salut
(
"hello "
, 6
)
[hello][hello][hello][hello][hello][hello]
Un second point à se rappeler est qu’une fonction peut être incluse à l’intérieur d’une autre fonction. Et que la fonction interne a quand même accès aux variables présentes dans la fonction externe…
2.
3.
4.
5.
6.
7.
8.
9.
>>>
def
bonjour
(
s, n=
1
):
... def
maj
(
):
... print
(
"[
%s
]"
%
s.upper
(
) *
((
n//
2
)) # La fonction interne connait "n"
... maj
(
)
... print
(
"[
%s
]"
%
s *
n)
...
>>>
bonjour
(
"hello"
, 4
)
[HELLO][HELLO]
[hello][hello][hello][hello]
Ces deux caractéristiques permettent alors par exemple à une fonction d’offrir un choix entre divers traitements, chaque choix étant dévolu à une fonction interne…
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
def
bonjour
(
espece=
"humain"
):
# On commence d’abord par créer les sous-fonctions des traitements offerts
def
chien
(
n=
1
): print
(
"[ouah]"
*
n)
def
chat
(
n=
1
): print
(
"[miaou]"
*
n)
def
humain
(
n=
1
): print
(
"[bonjour]"
*
n)
# Ensuite on retourne la sous-fonction associée à la caractéristique demandée
return
{
"chien"
: chien,
"chat"
: chat,
"humain"
: humain,
}[espece]
# bonjour()
>>>
# Pour s’en servir, on récupère la sous-fonction qu’elle renvoie
>>>
parler=
bonjour
(
"chien"
)
>>>
>>>
# Et ensuite on peut appeler la sous-fonction récupérée
>>>
parler
(
6
)
[ouah][ouah][ouah][ouah][ouah][ouah]
>>>
>>>
# On peut aussi faire le tout en une seule opération
>>>
bonjour
(
"chat"
)(
5
)
[miaou][miaou][miaou][miaou][miaou]
Le troisième point important pour comprendre les décorateurs est que puisqu’une fonction est un objet, on peut alors aussi la passer comme simple argument à une autre fonction qui aura alors la possibilité de l'exécuter à son gré.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
def
compter
(
n):
for
i in
range(
1
, n+
1
): print
(
i, end=
''
)
def
decorateur
(
fct, n):
print
(
"Attention, la fonction
%s
va s'exécuter avec
%d
"
%
(
fct, n))
fct
(
n)
print
(
"Fonction
%s
terminée"
%
fct)
>>>
decorateur
(
compter, 5
)
Attention, la fonction <
function compter at 0x7fb06479f5f0
>
va s’exécuter avec 5
1
2
3
4
5
Fonction <
function compter at 0x7fb06479f5f0
>
terminée
Ceci est donc l'exemple de base pour comprendre le décorateur : une surcouche qui ira exécuter du code avant et après la fonction qu’elle reçoit, tout en exécutant aussi la fonction qu’elle reçoit et sans que le développeur ait besoin de modifier la fonction en question.
XXIV-2. Un premier décorateur▲
Prenons pour l’exemple une fonction assez simple : un calcul de factorielle…
Le décorateur qui va décorer cette fonction factorielle doit alors recevoir ladite fonction afin de pouvoir l’encapsuler dans son propre traitement tout en lui faisant faire quand même son travail de factorielle.
Toutefois, et c'est là un point important, ce traitement ne doit pas se faire quand le décorateur est appelé, mais quand c'est la factorielle qui est appelée !
C'est pourquoi le décorateur est obligé de définir une fonction interne qui se chargera de la décoration proprement dite. Et le décorateur, lors de son appel, se contente de retourner cette fonction interne qui prendra alors la place de la factorielle d'origine.
Mais comme la fonction interne doit prendre la place de la factorielle, il est nécessaire qu'elle fasse le même travail que cette factorielle, donc qu'elle puisse appeler ladite factorielle et récupérer son résultat. En un mot qu’elle connaisse cette factorielle.
Ce qui donne le code suivant :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
# Création d’un décorateur sur la fonction factorielle qu’il recevra en paramètre
def
decorateur
(
fct):
# Le wrapper qui encapsule la factorielle dans un traitement spécifique
# Cette factorielle recevant un paramètre, il doit en faire de même
def
wrapper
(
n):
# Il appelle ladite factorielle en lui passant à l’identique le paramètre reçu
# Notez que la variable "fct" connue du décorateur est donc connue du wrapper
r=
fct
(
n)
# Un affichage quelconque pour l’exemple
print
(
"Je décore
%s
(
%s
) - Le résultat est
%s
"
%
(
fct, n, r))
# Il renvoie le résultat calculé par la factorielle
return
r
# wrapper()
# Ici on est dans le décorateur. Il va alors simplement renvoyer le wrapper
return
wrapper
# decorateur()
Ensuite, il ne reste plus qu’à remplacer la factorielle initiale par le wrapper renvoyé par le décorateur.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
>>>
# D'abord création de la fonction factorielle
>>>
from
functools import
reduce
>>>
def
facto
(
n): return
reduce(
lambda
x, y: x *
y, range(
1
, n+
1
)) if
n else
1
...
>>>
# Ensuite remplacement de la factorielle par le wrapper renvoyé par le décorateur
>>>
facto=
decorateur
(
facto)
>>>
>>>
# Enfin test de la fonction décoréé
>>>
facto
(
10
)
Je décore <
function facto at 0x7f2693b26bf8
>
(
10
) -
Le résultat est 3628800
3628800
On a donc bien finalement le message provenant du wrapper, ainsi que le résultat de la fonction d’origine (appelée en réalité par le wrapper, mais ce détail reste transparent pour l’utilisateur). C’est un décorateur.
Arrivé ici, Python offre aussi un sucre syntaxique au travers de la syntaxe @decorateur
qui remplace l’instruction fonction=
decorateur
(
fonction).
Ainsi, le code précédent devient :
2.
3.
4.
5.
6.
7.
8.
9.
>>>
# Création de la fonction factorielle directement décorée
>>>
from
functools import
reduce
>>>
@decorateur
... def
facto
(
n): return
reduce(
lambda
x, y: x *
y, range(
1
, n+
1
)) if
n else
1
...
>>>
# Ensuite test de la fonction décorée
>>>
facto
(
10
)
Je décore <
function facto at 0x7f2693b26bf8
>
(
10
) -
Le résultat est 3628800
3628800
Bien entendu, un même décorateur peut s'appliquer sur différentes fonctions qui auraient la même signature (ici un paramètre)…
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
# Ce même décorateur appliqué sur une fonction carre()
@decorateur
def
carre
(
n): return
n**
2
# Ce même décorateur appliqué sur une fonction cube()
@decorateur
def
cube
(
n): return
n**
3
>>>
carre
(
10
)
Je décore <
function carre at 0x7f2bb0b81950
>
(
10
) -
Le résultat est 100
100
>>>
cube
(
10
)
Je décore <
function cube at 0x7f2bb0ab3e18
>
(
10
) -
Le résultat est 1000
1000
… et une même fonction peut être décorée par différents décorateurs.
XXIV-3. Un peu plus complexe : un décorateur plus universel▲
Ce premier décorateur possède l'inconvénient majeur de ne pouvoir être appliqué qu'à une fonction recevant un seul argument et échouera à décorer une fonction plus complexe…
2.
3.
4.
5.
6.
7.
8.
9.
# Application du même décorateur sur une fonction somme() à 2 paramètres
@decorateur
def
somme
(
a, b): return
a+
b
>>>
somme
(
5
, 6
)
Traceback (
most recent call last):
File "<stdin>"
, line 1
, in
<
module>
TypeError
: wrapper
(
) takes 1
positional argument but 2
were given
>>>
Toutefois, rappelons qu'il existe deux types de paramètres très particuliers *
args et **
kwargs qui permettent d'adapter une fonction à toute combinaison d'arguments reçus.
Exemple d’un second décorateur qui devient universel, car il ne se préoccupe plus des arguments envoyés à la fonction, il les lui passe tels quels :
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.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
# Le décorateur sur la fonction
def
decorateur
(
fct):
# Le wrapper qui encapsule la fonction dans un traitement spécifique
# Il récupère en vrac tous les arguments passés à la fonction
def
wrapper
(*
args, **
kwargs):
# Il appelle la fonction en lui passant à l’identique les arguments reçus
r=
fct
(*
args, **
kwargs)
# Un affichage quelconque
print
(
"Je décore
%s
(
%s
,
%s
) - Le résultat est
%s
"
%
(
fct, args, kwargs, r))
# Il renvoie le résultat calculé par la fonction
return
r
# wrapper()
# Ici on est dans le décorateur. Il va alors renvoyer le wrapper sur la fonction
return
wrapper
# decorateur()
# Application sur une fonction recevant un argument
from
functools import
reduce
@decorateur
def
facto
(
n): return
reduce(
lambda
x, y: x *
y, range(
1
, n+
1
)) if
n else
1
>>>
facto
(
10
)
Je décore <
function facto at 0x7f6cd7405ea0
>
((
10
,), {}) -
Le résultat est 3628800
3628800
# Application sur une fonction recevant deux arguments
@decorateur
def
somme
(
a, b): return
a+
b
>>>
somme
(
5
, 6
)
Je décore <
function somme at 0x7f6cd7337ea0
>
((
5
, 6
), {}) -
Le résultat est 11
11
# Et on peut nommer les arguments, ils passent alors par kwargs
>>>
somme
(
b=
5
, a=
6
)
Je décore <
function somme at 0x7f6cd7337ea0
>
((
), {"a"
: 6
, "b"
: 5
}) -
Le résultat est 11
11
XXIV-4. Le danger du décorateur▲
Une fois qu’on a compris ce principe, on commence à imaginer toute une gamme de possibilités. Cependant, le décorateur présente un danger si on l’associe à une fonction récursive. N’oublions pas que la fonction est intégralement remplacée par le décorateur. L’appel récursif entrainera alors l’appel du décorateur de façon récursive.
Reprenons l’exemple de la factorielle, mais en la codant de façon récursive…
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
# Création de la fonction factorielle de façon récursive
@decorateur
def
facto
(
n): return
n *
facto
(
n-
1
) if
n else
1
>>>
facto
(
5
)
Je décore <
function facto at 0x7f99bdde0668
>
((
0
,), {}) -
Le résultat est 1
Je décore <
function facto at 0x7f99bdde0668
>
((
1
,), {}) -
Le résultat est 1
Je décore <
function facto at 0x7f99bdde0668
>
((
2
,), {}) -
Le résultat est 2
Je décore <
function facto at 0x7f99bdde0668
>
((
3
,), {}) -
Le résultat est 6
Je décore <
function facto at 0x7f99bdde0668
>
((
4
,), {}) -
Le résultat est 24
Je décore <
function facto at 0x7f99bdde0668
>
((
5
,), {}) -
Le résultat est 120
120
Le résultat du programme n’est peut-être pas ce que voulait le concepteur du décorateur. Ou celui que voulait l’utilisateur du décorateur.
Pour supprimer ce désagrément, il faut veiller à ce que toute fonction récursive soit toujours encapsulée dans une couche principale non récursive. Ce point est expliqué plus en détail au chapitre sur l'optimisation d'une fonction récursive. Ainsi, c'est la couche principale qui sera décorée et non la fonction récursive…
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
# Création de la fonction factorielle de façon récursive
@decorateur
def
facto
(
n):
# La sous-fonction moteur du calcul récursif
calcul_r=
lambda
n: n *
calcul_r
(
n-
1
) if
n else
1
# Renvoi calcul récursif de la factorielle
return
calcul_r
(
n)
# facto()
>>>
facto
(
5
)
Je décore <
function facto at 0x7f99bdde0668
>
((
5
,), {}) -
Le résultat est 120
120
XXIV-5. Exemple : un décorateur qui gère les appels déjà connus▲
Exemple d’un décorateur qui va optimiser les appels. Tout appel déjà connu ne sera alors pas recalculé.
Pour pouvoir stocker le tableau des appels, on se sert du fait que le décorateur est une fonction et qu’une fonction est aussi un objet sur lequel on peut rajouter des attributs spécifiques.
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.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
#!/usr/bin/env python3
# coding: utf-8
# Un décorateur qui va optimiser les appels
# Tout appel déjà connu évite alors de recalculer le résultat
def
optimize
(
fct):
# Création du stockage des résultats (fixé à 3 pour l'exemple)
# C'est une liste de dictionnaires contenant les arguments de la fonction et son résultat result=[{"n" : None, "r" : None},] * 3
# Le wrapper qui encapsule la fonction dans un traitement spécifique
# Il récupère en vrac tous les arguments passés à la fonction
def
wrapper
(*
args, **
kwargs):
# Si les arguments sont déjà connus (donc dans l’un des dictionnaires de la liste)
for
x in
result:
if
x["n"
] ==
(
args, kwargs):
# Récupération du résultat (qui est aussi dans le même dictionnaire)
r=
x["r"
]
print
(
"Résultat de (
%s
,
%s
) déjà connu =>
%s
"
%
(
args, kwargs, r))
break
# if
else
:
# Le résultat n'est pas encore connu - Il est alors calculé
r=
fct
(*
args, **
kwargs)
print
(
"Calcul de (
%s
,
%s
) - Le résultat est
%s
"
%
(
args, kwargs, r))
# On supprime le premier résultat pour pouvoir enregistrer le dernier
del
result[0
]
result.append
(
{"n"
: (
args, kwargs), "r"
: r})
# for
# Affichage du tableau des résultats (pour démo)
print
(
"
\n
"
.join
(
str(
x) for
x in
result))
# Renvoi du résultat (qui a été récupéré ou calculé)
return
r
# wrapper()
# Ici, on est dans le décorateur. Il va alors renvoyer le wrapper sur la fonction
return
wrapper
# optimize()
from
functools import
reduce
@optimize
def
facto
(
n): return
reduce(
lambda
x, y: x *
y, range(
1
, n+
1
)) if
n else
1
# Exemple de calcul avec des valeurs dupliquées
for
i in
(
7
, 5
, 9
, 5
, 9
, 1
, 7
, 9
): print
(
"facto(
%d
)=
%d
"
%
(
i, facto
(
i)), end=
"
\n\n
"
)
Et le résultat…
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.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
Calcul de ((
7
,), {}) -
Le résultat est 5040
{'n'
: None
, 'r'
: None
}
{'n'
: None
, 'r'
: None
}
{'n'
: ((
7
,), {}), 'r'
: 5040
}
facto
(
7
)=
5040
Calcul de ((
5
,), {}) -
Le résultat est 120
{'n'
: None
, 'r'
: None
}
{'n'
: ((
7
,), {}), 'r'
: 5040
}
{'n'
: ((
5
,), {}), 'r'
: 120
}
facto
(
5
)=
120
Calcul de ((
9
,), {}) -
Le résultat est 362880
{'n'
: ((
7
,), {}), 'r'
: 5040
}
{'n'
: ((
5
,), {}), 'r'
: 120
}
{'n'
: ((
9
,), {}), 'r'
: 362880
}
facto
(
9
)=
362880
Résultat de ((
5
,), {}) déjà connu =>
120
{'n'
: ((
7
,), {}), 'r'
: 5040
}
{'n'
: ((
5
,), {}), 'r'
: 120
}
{'n'
: ((
9
,), {}), 'r'
: 362880
}
facto
(
5
)=
120
Résultat de ((
9
,), {}) déjà connu =>
362880
{'n'
: ((
7
,), {}), 'r'
: 5040
}
{'n'
: ((
5
,), {}), 'r'
: 120
}
{'n'
: ((
9
,), {}), 'r'
: 362880
}
facto
(
9
)=
362880
Calcul de ((
1
,), {}) -
Le résultat est 1
{'n'
: ((
5
,), {}), 'r'
: 120
}
{'n'
: ((
9
,), {}), 'r'
: 362880
}
{'n'
: ((
1
,), {}), 'r'
: 1
}
facto
(
1
)=
1
Calcul de ((
7
,), {}) -
Le résultat est 5040
{'n'
: ((
9
,), {}), 'r'
: 362880
}
{'n'
: ((
1
,), {}), 'r'
: 1
}
{'n'
: ((
7
,), {}), 'r'
: 5040
}
facto
(
7
)=
5040
Résultat de ((
9
,), {}) déjà connu =>
362880
{'n'
: ((
9
,), {}), 'r'
: 362880
}
{'n'
: ((
1
,), {}), 'r'
: 1
}
{'n'
: ((
7
,), {}), 'r'
: 5040
}
facto
(
9
)=
362880
À noter : ce décorateur existe déjà dans le module functools et se nomme functools.lru_cache.
2.
3.
4.
5.
6.
7.
8.
9.
10.
>>>
from
functools import
lru_cache
>>>
@lru_cache
(
maxsize=
10
)
>>>
def
facto
(
n): return
n *
facto
(
n-
1
) if
n else
1
>>>
print
(
facto
(
8
)) # Toutes les valeurs de 0 à 8 sont calculées
40320
>>>
print
(
facto
(
5
)) # Valeur déjà calculée, ne sera donc pas recalculée
120
>>>
print
(
facto
(
10
)) # Seules les valeurs 9 et 10 seront calculées
3628800
XXIV-6. Encore plus complexe : donner des arguments au décorateur▲
L’exemple précédent utilise un tableau des appels défini arbitrairement à 3 éléments par le créateur du décorateur. Ce qui limite un peu son utilisation dans des configurations diverses comme cela arrive souvent dans le monde du développement collaboratif et hétérogène. Peut-être que certains utilisateurs qui travaillent sur des ordinateurs puissants aimeraient pouvoir optimiser le calcul sur 10, 100, 1000, 10000 résultats…
C'est possible en décorant le décorateur lui‑même. Ainsi, en encapsulant le décorateur qui gère les résultats de la fonction (qu'on nommera décorateurFct) dans un autre décorateur (nommé décorateurParam), ce dernier peut alors gérer des arguments donnés par l’utilisateur. Ainsi, les arguments donnés à décorateurParam pourront servir à paramétrer décorateurFct.
Exemple : paramétrer le tableau de résultats. Le décorateur recevra comme paramètre la taille allouée au tableau (qui pourra toutefois être à 0 s'il décide de ne pas optimiser) ainsi qu'un booléen de contrôle indiquant si l’on doit afficher ou pas le tableau…
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.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
#!/usr/bin/env python3
# coding: utf-8
# Le décorateur qui récupère des paramètres de décoration demandés par l'appelant
def
optimize
(
size=
0
, debug=
False
):
# Création du stockage des résultats (la taille étant donnée par l'appelant)
result=
[{"n"
: None
, "r"
: None
},] *
size
# Le décorateur qui optimise la fonction
def
__optimizeFct
(
fct):
# Le wrapper qui gère tous les schémas d'arguments possibles
def
__wrapper
(*
args, **
kwargs):
# Récupération du résultat si déjà connu
# Notez l'utilisation d'un "tuple en intension" créé à partir du tableau de résultats
# Cela permet alors d'y appliquer la méthode "index()" à la volée
# Cette méthode "index()" renverra une exception si le résultat n'est pas connu
try
:
idx=
tuple(
x["n"
] for
x in
result).index
((
args, kwargs))
r=
result[idx]["r"
]
print
(
"Résultat de (
%s
,
%s
) déjà connu =>
%s
"
%
(
args, kwargs, r))
except
ValueError
:
# Le résultat n'est pas encore connu - Il est alors calculé
r=
fct
(*
args, **
kwargs)
print
(
"Calcul de (
%s
,
%s
) - Le résultat est
%s
"
%
(
args, kwargs, r))
# Si l'utilisateur a défini une taille d'optimisation
if
size:
del
result[0
]
result.append
(
{"n"
: (
args, kwargs), "r"
: r})
# if
# try
# Si l’utilisateur veut tracer le tableau résultats
if
debug: print
(
"
\n
"
.join
(
str(
x) for
x in
result) if
size else
"
{}
"
)
# Renvoi du résultat
return
r
# __wrapper()
# Ici, on est dans le décorateur dédié à la fonction - Renvoi wrapper fonction
return
__wrapper
# __optimizeFct()
# Ici, on est dans le décorateur dédié aux arguments – Renvoi décorateur fonction
return
__optimizeFct
# optimize()
from
functools import
reduce
@optimize
(
size=
5
, debug=
False
) # L'utilisateur veut optimiser sur 5 calculs
def
facto
(
n): return
reduce(
lambda
x, y: x *
y, range(
1
, n+
1
)) if
n else
1
XXIV-7. L'objet décorateur▲
Ce mécanisme de décoration à deux niveaux commence à devenir complexe (3 couches d’imbrications). L'utilisation d'un objet pour créer un décorateur permet de le simplifier en gérant et organisant les divers éléments (arguments du décorateur, arguments de la fonction, etc.). En effet, la création de l'objet permettra de gérer ses arguments et son appel permet de gérer la fonction (car souvenons-nous qu'un objet peut être appelable et c'est lors de l'appel du décorateur qu'on remplace la fonction initiale par le wrapper du décorateur).
Le même exemple avec un objet décorateur…
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.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
#!/usr/bin/env python3
# coding: utf-8
# Un objet décorateur qui va optimiser les appels
# Tout appel déjà connu évite alors de recalculer le résultat
class
cOptimize:
# À la création du décorateur, on traite ses paramètres
def
__init__
(
self, size=
0
, debug=
False
):
self.__result=
list((
{"n"
: None
, "r"
: None
},) *
size)
self.__debug=
debug
# __init__()
# À l’appel du décorateur, on traite la fonction qu'il décore
def
__call__
(
self, fct):
# Le wrapper de la fonction
def
wrapper
(*
args, **
kwargs):
# Gestion résultat déjà connu
try
:
idx=
tuple(
x["n"
] for
x in
self.__result).index
((
args, kwargs))
r=
self.__result[idx]["r"
]
print
(
"Résultat de (
%s
,
%s
) déjà connu =>
%s
"
%
(
args, kwargs, r))
except
ValueError
:
r=
fct
(*
args, **
kwargs)
print
(
"Calcul de (
%s
,
%s
) - Le résultat est
%s
"
%
(
args, kwargs, r))
if
self.__result:
del
self.__result[0
]
self.__result.append
(
{"n"
: (
args, kwargs), "r"
: r})
# if
# try
if
self.__debug:
print
(
"
\n
"
.join
(
str(
x) for
x in
self.__result) if
self.__result else
"
{}
"
)
# Renvoi du résultat
return
r
# wrapper()
# On est revenu à l’appel du décorateur - Renvoi wrapper fonction
return
wrapper
# __call__()
# class cOptimize
from
functools import
reduce
@cOptimize
(
size=
5
, debug=
False
)
def
facto
(
n): return
reduce(
lambda
x, y: x *
y, range(
1
, n+
1
)) if
n else
1
# Ou alors (écriture originelle)
def
facto
(
n): return
reduce(
lambda
x, y: x *
y, range(
1
, n+
1
)) if
n else
1
facto=
cOptimize
(
size=
7
, debug=
True
)(
facto)
XXIV-8. Conclusion▲
De par sa souplesse d’utilisation, le décorateur se place comme un outil assez fondamental dans le développement d’applications Python. Il permet de programmer des compteurs d’appels, des optimiseurs, des debuggueurs, des logs, des chronomètres, des vérificateurs d'arguments, etc. Et tout cela sans avoir à modifier les fonctions à décorer qui peuvent même provenir de librairies externes et ne sont par conséquent pas modifiables.
XXIV-9. Dernier détail…▲
Le décorateur ayant remplacé la fonction originelle, celle-ci perd toute son identité jusqu'à son nom.
Reprenons l'exemple du premier décorateur de ce chapitre…
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
# Le décorateur sur la fonction
def
decorateur
(
fct):
# Le wrapper qui encapsule la fonction dans un traitement spécifique
def
wrapper
(*
args, **
kwargs):
# Il renvoie juste le résultat calculé par la fonction
return
fct
(*
args, **
kwargs)
# wrapper()
# Ici on est dans le décorateur. Il va alors renvoyer le wrapper sur la fonction
return
wrapper
# decorateur()
… et regardons ce qui se passe quand la fonction décorée veut afficher son identité :
2.
3.
4.
5.
6.
7.
8.
9.
# Création d'une fonction quelconque
@decorateur
def
somme
(
a, b):
print
(
somme) # La fonction affiche son identité (son nom quoi)
return
a+
b
>>>
somme
(
2
, 3
)
<
function decorateur.<
locals>
.wrapper at 0x7fd5acdf9b70
>
5
Cette identité n'est plus celle de la fonction d'origine somme
(
), mais celle du wrapper qui l'a remplacée.
Le module functools (déjà vu dans ce chapitre) contient un décorateur wraps permettant à la fonction décorée de garder son identité.
Il suffit de rajouter l'instruction @wraps
(
) en y donnant la fonction décorée pour qu'elle conserve son identité dans le décorateur.
Exemple :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
from
functools import
wraps
# Le décorateur sur la fonction
def
decorateur
(
fct):
# Tout d'abord, on conserve l'identité de la fonction décorée
@wraps
(
fct)
# Ensuite on écrit le wrapper qui va décorer la fonction
def
wrapper
(*
args, **
kwargs):
# Ceci est juste un exemple pour montrer "@wraps"
# Ce décorateur ne fait donc rien et renvoie simplement le résultat de la fonction
return
fct
(*
args, **
kwargs)
# wrapper()
# Ici on est dans le décorateur. Il va alors renvoyer le wrapper sur la fonction
return
wrapper
# decorateur()
… et regardons la différence quand la fonction décorée veut afficher son identité…
2.
3.
4.
5.
6.
7.
8.
9.
# Création d'une fonction quelconque
@decorateur
def
somme
(
a, b):
print
(
somme) # La fonction affiche son identité
return
a+
b
>>>
somme
(
2
, 3
)
<
function somme at 0x7fd5acdf9b70
>
5
… et voilà. Tout est devenu transparent pour tout le monde.