IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)

Python, de zéro


précédentsommairesuivant

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.

 
Sélectionnez
1.
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é.

 
Sélectionnez
1.
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…

 
Sélectionnez
1.
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…

 
Sélectionnez
1.
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é.

 
Sélectionnez
1.
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…

 
Sélectionnez
1.
2.
3.
4.
5.
>>> from functools import reduce                        # Inutile dans Python 2
>>> def facto(n): return reduce(lambda x, y: x * y, range(1, n+1)) if n else 1
...
>>> print(facto(10))
3628800

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 :

 
Sélectionnez
1.
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.

 
Sélectionnez
1.
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 :

 
Sélectionnez
1.
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)…

 
Sélectionnez
1.
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.

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
from functools import reduce

# Fonction factorielle décorée plusieurs fois (vous écrirez ces décorateurs vous‑mêmes)
@decorateurA
@decorateurB
@decorateurC
def facto(n): return reduce(lambda x, y: x * y, range(1, n+1)) if n else 1

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…

 
Sélectionnez
1.
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 :

 
Sélectionnez
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.
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…

 
Sélectionnez
1.
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…

 
Sélectionnez
1.
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.

 
Sélectionnez
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.
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…

 
Sélectionnez
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.
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.

 
Sélectionnez
1.
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…

 
Sélectionnez
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.
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…

 
Sélectionnez
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.
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…

 
Sélectionnez
1.
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é :

 
Sélectionnez
1.
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 :

 
Sélectionnez
1.
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é…

 
Sélectionnez
1.
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.


précédentsommairesuivant

Copyright © 2022 Svear (svear@free.fr) Permission est accordée de copier, distribuer ou modifier ce document selon les termes de la « Licence de Documentation Libre GNU » (GNU Free Documentation License), version 1.1 ou toute version ultérieure publiée par la Free Software Foundation.