Les décorateurs en Python
Après avoir compris les fermetures (closures) dans le chapitre précédent, les décorateurs en représentent une application particulière. La différence principale réside dans le fait que la fonction externe ne reçoit pas un type de variable ordinaire comme paramètre, mais plutôt le nom d'une fonction. Les décorateurs sont généralement utilisés pour ajouter ou étendre les fonctionnalités d'une fonction sans modifier son code interne, par exemple pour mesurer le temps d'exécution d'une fonction, enregistrer des informations de journal avant et après son appel, ou encore ajouter des vérifications de permissions ou de données.
Voici un exemple simple de décorateur qui ajoute une vérification de permissions avant d'exécuter la fonction test1 :
def verificateur_permissions(func):
def fonction_intermediaire():
print('--------Vérification des permissions--------')
func()
return fonction_intermediaire
@verificateur_permissions
def test1():
print('-------test1 appelé-------')
test1()<br></br><br></br># Résultat d'exécution :
--------Vérification des permissions------- -------test1 appelé--------
Principe de fonctionnement des décorateurs
def verificateur_permissions(func):
def fonction_intermediaire():
print('--------Validation des données--------')
print('La variable func pointe vers :', func)
func()
return fonction_intermediaire
def test1():
print('-------test1 appelé-------')
print('Avant décoration, test1 pointe vers :', test1)
print('1. Création d\'une variable ret pour recevoir la valeur de retour de verificateur_permissions')
ret = verificateur_permissions(test1)
print('2. Maintenant, ret pointe vers :', ret)
print('3. Appel de ret')
ret()
# Résultat d'exécution:
Avant décoration, test1 pointe vers: <function test1 at 0x000001F2220BA1F0>
1. Création d'une variable ret pour recevoir la valeur de retour de verificateur_permissions
2. Maintenant, ret pointe vers: <function verificateur_permissions.<locals>.fonction_intermediaire at 0x000001F2220BA280>
3. Appel de ret
--------Validation des données--------
La variable func pointe vers: <function test1 at 0x000001F2220BA1F0>
-------test1 appelé--------
- On définit une fermeture : la fonction externe verificateur_permissions prend func comme paramètre, et retourne la fonction interne fonction_intermediaire.
- On définit une fonction ordinaire test1, où le nom de la fonction est aussi le nom de la variable. À ce stade, la variable test1 pointe vers l'adresse mémoire de la fonction test1....BA1F0.
- Appel de verificateur_permissions :
3.1. On passe test1 en paramètre, donc le paramètre formel func de verificateur_permissions pointe vers la même adresse que test1, c'est-à-dire....BA1F0.
3.2. On définit une fonction nommée fonction_intermediaire, crée son adresse mémoire correspondante, et la retourne à la variable ret. Ainsi, ret pointe vers l'adresse....BA280 de la fonction fonction_intermediaire.
- Appel de la fonction ret, c'est-à-dire exécution du code dans fonction_intermediaire :
4.1. Affichage de la validation des données et de l'adresse pointée par func.
4.2. Appel de func(), qui pointe vers la fonction test1, donc on exécute le code de test1, affichant '-------test1 appelé--------'.
À ce stade, l'exécution est terminée, avec succès, on a affiché le code de validation des données avant l'appel de test1. Cependant, au lieu d'appeler test1, nous devons maintenant appeler ret pour ajouter la fonctionnalité de validation, ce qui modifie toujours le code original. Modifions davantage le programme. Au lieu d'utiliser la variable ret, nous pourrions utiliser n'importe quel autre nom comme aaa, bbb ou foo. Maintenant, remplaçons le nom de variable par test1 dans la version précédente du code :
def verificateur_permissions(func):
def fonction_intermediaire():
print('--------Validation des données--------')
print('La variable func pointe vers :', func)
func()
return fonction_intermediaire
def test1():
print('-------test1 appelé-------')
print('Avant décoration, test1 pointe vers :', test1)
print('Implémentation manuelle du décorateur :')
print('1. Création d\'une variable ret pour recevoir la valeur de retour de verificateur_permissions')
ret = verificateur_permissions(test1)
print('2. Maintenant, ret pointe vers :', ret)
print('3. Appel de ret')
ret()
print('=' * 30, 'Je suis un séparateur', '=' * 30)
print('1. Remplacement du nom de variable ret par test1')
test1 = verificateur_permissions(test1)
print('2. Maintenant, test1 pointe vers :', test1)
print('3. Appel de test1')
test1()
Résultat d'exécution :
Avant décoration, test1 pointe vers: <function test1 at 0x0000019042CA0280>
Implémentation manuelle du décorateur:
1. Création d'une variable ret pour recevoir la valeur de retour de verificateur_permissions
2. Maintenant, ret pointe vers: <function verificateur_permissions.<locals>.fonction_intermediaire at 0x0000019042CA0310>
3. Appel de ret
--------Validation des données--------
La variable func pointe vers: <function test1 at 0x0000019042CA0280>
-------test1 appelé--------
============================== Je suis un séparateur ==============================
1. Remplacement du nom de variable ret par test1
2. Maintenant, test1 pointe vers: <function verificateur_permissions.<locals>.fonction_intermediaire at 0x0000019042CA03A0>
3. Appel de test1
--------Validation des données--------
La variable func pointe vers: <function test1 at 0x0000019042CA0280>
-------test1 appelé--------
- Lors de l'appel de la fonction verificateur_permissions, on retourne la référence de la fonction fonction_intermediaire à la variable test1. Avant l'appel, la variable test1 pointait vers l'adresse de la fonction test1...CA0280 (notez que l'adresse mémoire créée pour le même nom de fonction peut être différente à chaque exécution). Après l'appel, la variable test1 pointe maintenant vers l'adresse de fonction_intermediaire...CA03A0. Chaque fois que verificateur_permissions est appelé, le paramètre formel func pointe vers l'adresse de la fonction test1...CA0280. Par conséquent, bien que la variable test1 ne pointe plus vers l'adresse originale de la fonction test1, il y aura toujours un paramètre formel pointant vers l'adresse de la fonction test1 et la conservant. C'est ce que l'on appelle la fermeture : lorsque la fonction externe de la fermeture est appelée, le paramètre passé est sauvegardé et utilisé lors de l'appel de la fonction interne.
- Après que verificateur_permissions retourne la référence de la fonction fonction_intermediaire à la variable test1, l'appel suivant de test1() est en réalité l'appel de la méthode fonction_intermediaire. Dans fonction_intermediaire, on affiche la validation des données et on appelle func(), qui pointe vers la fonction test1, c'est-à-dire qu'on appelle la fonction test1 et on affiche son contenu.
Le processus consistant à changer la variable test1 de pointer vers la fonction test1 à pointer vers la valeur de retour de verificateur_permissions, c'est-à-dire fonction_intermediaire, correspond au code test1 = verificateur_permissions(test1), qui est le processus de décoration. Pour simplifier l'écriture sans avoir à écrire test1 = verificateur_permissions(test1) chaque fois, on peut ajouter @verificateur_permissions avant la définition de la fonction test1. Autrement dit, @verificateur_permissions est équivalent à test1 = verificateur_permissions(test1).
Amélioration des décorateurs
- Ajout de paramètres et de valeurs de retour à la fonction interne du décorateur
Un décorateur peut être utilisé pour décorer plusieurs fonctions, qui peuvent avoir des paramètres différents ou non avoir de valeur de retour. Pour que le décorateur s'adapte à toutes les fonctions décorées, il faut lui ajouter des paramètres et des valeurs de retour, comme ceci :
# Pour que le décorateur s'adapte à tous les paramètres et valeurs de retour des fonctions décorées
# 1. Il faut écrire *args et **kwargs dans fonction_intermediaire
# 1.1 Notez que les noms de paramètres utilisés sont args et kwargs, et non *args et **kwargs
# Ici, les * et ** dans les paramètres sont pour que l'interpréteur Python sépare les paramètres excédentaires en tuple et dictionnaire
# 1.2 Lors de l'appel de func avec les paramètres, on ajoute aussi * et **. Ici, * et ** servent à déballer args et kwargs
# Cela revient à passer les paramètres déballés dans la même dimension, comme func('a', 'b', 'c', d=1, e='2')
# Si l'appel à func se fait sans *, cela revient à passer func(('a', 'b', 'c'), {'d': 1, 'e': '2'})
# L'interpréteur considérerait alors qu'on passe deux valeurs : un tuple et un dictionnaire
# Puis, dans func, lors du traitement des paramètres, le premier tuple serait reçu par para, et le second dictionnaire par *args, ce qui donnerait un résultat différent de l'attendu
# 2. Ajouter un return avant d'appeler func dans fonction_intermediaire, ainsi si la fonction décorée a aussi un return, le résultat final peut être retourné, comme dans test2
def verificateur_permissions(func):
def fonction_intermediaire(*args, **kwargs):
print('------Validation des données------')
print('args', args)
print('kwargs', kwargs)
return func(*args, **kwargs)
return fonction_intermediaire
@verificateur_permissions
def test1(para, *args, **kwargs):
print('------Appel de test1------')
print('------para------', para)
print('------*args------', args)
print('------**kwargs------', kwargs)
@verificateur_permissions
def test2(para, *args, **kwargs):
print('------Appel de test2------')
print('------para------', para)
print('------*args------', args)
print('------**kwargs------', kwargs)
return 'ok...'
test1(1)
print('=' * 30, 'Je suis un séparateur', '=' * 30)
test1('a')
print('=' * 30, 'Je suis un séparateur', '=' * 30)
test1('a', 'b', 'c')
print('=' * 30, 'Je suis un séparateur', '=' * 30)
test1('a', 'b', 'c', d=1, e='2')
print('=' * 30, 'Je suis un séparateur', '=' * 30)
print(test2('a', 'b', 'c', d=1, e='2'))
Résultat d'exécution :
------Validation des données------
args (1,)
kwargs {}
------Appel de test1------
------para------ 1
------*args------ ()
------**kwargs------ {}
============================== Je suis un séparateur ==============================
------Validation des données------
args ('a',)
kwargs {}
------Appel de test1------
------para------ 'a'
------*args------ ()
------**kwargs------ {}
============================== Je suis un séparateur ==============================
------Validation des données------
args ('a', 'b', 'c')
kwargs {}
------Appel de test1------
------para------ 'a'
------*args------ ('b', 'c')
------**kwargs------ {}
============================== Je suis un séparateur ==============================
------Validation des données------
args ('a', 'b', 'c')
kwargs {'d': 1, 'e': '2'}
------Appel de test1------
------para------ 'a'
------*args------ ('b', 'c')
------**kwargs------ {'d': 1, 'e': '2'}
============================== Je suis un séparateur ==============================
------Validation des données------
args ('a', 'b', 'c')
kwargs {'d': 1, 'e': '2'}
------Appel de test2------
------para------ 'a'
------*args------ ('b', 'c')
------**kwargs------ {'d': 1, 'e': '2'}
ok...
- Ajout de paramètres au décorateur
Actuellement, lorsque le décorateur décore des fonctions différentes, le contenu ajouté devant est identique. Peut-on ajouter des fonctionnalités différentes aux fonctions décorées en utilisant le même décorateur ? Par exemple, ajouter une validation de données spécifique à test1, et une validation différente à test2.
Les paramètres de la fonction externe du décorateur sont actuellement fixes pour recevoir la fonction décorée, et les paramètres de la fonction interne sont également fixes pour recevoir les paramètres passés à la fonction décorée. Il faut donc ajouter une couche supplémentaire avec la fonction parent_func, où on définit un paramètre pour recevoir le nom de la fonction décorée, comme ceci :
def parent_func(nom):
def verificateur_permissions(func):
def fonction_intermediaire(*args, **kwargs):
print('------Validation des données de %s------' % nom)
return func(*args, **kwargs)
return fonction_intermediaire
return verificateur_permissions
@parent_func('test1')
def test1(para, *args, **kwargs):
print('------Appel de test1------')
@parent_func('test2')
def test2(para, *args, **kwargs):
print('------Appel de test2------')
return 'ok...'
test1(1)
print('=' * 30, 'Je suis un séparateur', '=' * 30)
print(test2('a', 'b', 'c', d=1, e='2'))
# Résultat d'exécution:
------Validation des données de test1------
------Appel de test1------
============================== Je suis un séparateur ==============================
------Validation des données de test2------
------Appel de test2------
ok...
- On définit une fonction supplémentaire parent_func qui reçoit un ou plusieurs paramètres selon les besoins spécifiques du métier. Cette fonction retourne le décorateur original.
- On remplace @verificateur_permissions par @parent_func(xxx). On peut comprendre cela comme : lorsque le code parent_func('test1') est exécuté, la fonction parent_func est appelée, ce qui retourne le décorateur original, qui décore ensuite la fonction.
Décoration d'une même fonction par plusieurs décorateurs
Parfois, nous avons besoin d'ajouter plusieurs décorateurs à une même fonction, comme un décorateur de vérification des permissions et un décorateur de validation des données, comme ceci :
def verificateur_permissions1(func):
print('----1. Début de la décoration de vérification des permissions----')
def fonction_intermediaire(*args, **kwargs):
print('------1. Vérification des permissions------')
return func(*args, **kwargs)
return fonction_intermediaire
def verificateur_permissions2(func):
print('----2. Début de la décoration de validation des données----')
def fonction_intermediaire(*args, **kwargs):
print('------2. Validation des données------')
return func(*args, **kwargs)
return fonction_intermediaire
@verificateur_permissions1
@verificateur_permissions2
def test1(para, *args, **kwargs):
print('------Appel de test1------')
print('Début de l\'appel de test1')
test1(1)
Résultat d'exécusion :
----2. Début de la décoration de validation des données----
----1. Début de la décoration de vérification des permissions----
Début de l'appel de test1
------1. Vérification des permissions------
------2. Validation des données------
------Appel de test1------
- On crée deux décorateurs, verificateur_permissions1 et verificateur_permissions2. Lors de la décoration de test1, @verificateur_permissions1 est écrit avant @verificateur_permissions2.
- D'après le résultat d'exécution, on observe deux phénomènes :
2.1. L'affichage 'Début de la décoration' se produit avant 'Début de l'appel', ce qui montre que le processus de décoration n'a pas lieu lors de l'appel de la fonction décorée, mais lorsque l'interpréteur atteint la ligne @xxxxx.
2.2. Lors de la décoration, l'ordre est de bas en haut, tandis que lors de l'appel, l'exécution des décorateurs se fait de haut en bas. On peut comprendre cela ainsi : l'interpréteur exécute de haut en bas. Lorsqu'il atteint le premier décorateur @verificateur_permissions1, il constate que la ligne suivante n'est pas la définition d'une fonction, donc il saute cette ligne et continue. Ensuite, il atteint @verificateur_permissions2, constate que la ligne suivante est la définition d'une fonction, et commence la décoration par verificateur_permissions2. Une fois terminé, il revient à la ligne précédente pour que verificateur_permissions1 décore le résultat précédent d'une couche supplémentaire. Le résultat est que la couche la plus externe est verificateur_permissions1, la couche intermédiaire est verificateur_permissions2, et la couche la plus interne est test1. Lors de l'appel de test1, l'exécution se fait de l'extérieur vers l'intérieur : on exécute d'abord verificateur_permissions1, puis verificateur_permissions2, et enfin test1.
Les décorateurs modifient certaines propriétés de la fonction originale
Après l'utilisation d'un décorateur, comme la variable pointant vers la fonction originale pointe maintenant vers la fonction définie à l'intérieur du décorateur, l'appel des méthodes name et doc de la fonction originale donnera les valeurs de la fonction interne du décorateur, comme ceci :
def verificateur_permissions(func):
def fonction_intermediaire():
"""Documentation de fonction_intermediaire"""
print('fonction_intermediaire')
func()
return fonction_intermediaire
@verificateur_permissions
def test():
"""Documentation de test"""
print('test')
print(test.__name__)
print(test.__doc__)
# Résultat d'exécution
# fonction_intermediaire
# Documentation de fonction_intermediaire
On voit que ce qui est affiché est l'attribut de la fonction interne fonction_intermediaire. Autrement dit, en définissant un décorateur qui agit sur une fonction, les métadonnées importantes comme le nom, la documentation, les annotations et la signature des paramètres sont perdues. Pour résoudre ce problème, lors de la définition de tout décorateur, on devrait utiliser le décorateur @wraps du module functools pour décorer la fonction interne. Par exemple :
from functools import wraps
def verificateur_permissions(func):
@wraps(func)
def fonction_intermediaire():
"""Documentation de fonction_intermediaire"""
print('fonction_intermediaire')
func()
return fonction_intermediaire
@verificateur_permissions
def test():
"""Documentation de test"""
print('test')
print(test.__name__)
print(test.__doc__)
# Résultat d'exécution
# test
# Documentation de test
Définition et utilisation de décorateurs dans une classe
from functools import wraps
class A:
def logger1(func):
"""Décorateur simple (sans paramètres)"""
@wraps(func)
def wrapper(self, *args, **kwargs):
print('Début de logger1')
func(self, *args, **kwargs)
print('Fin de logger1')
return wrapper
def logger2(log_data):
"""Décorateur complexe (avec paramètres)"""
def inner(func):
@wraps(func)
def wrapper(self, *args, **kwargs):
print('Début de logger2')
print(log_data)
func(self, *args, **kwargs)
print('Fin de logger2')
return wrapper
return inner
@logger1
def f1(self):
print('f1')
@logger2('Ceci est un paramètre')
def f2(self):
print('f2')
# Résultat du test
a = A()
a.f1()
print('-----------------------')
a.f2()
Résultat d'exécution :
Début de logger1
f1
Fin de logger1
-----------------------
Début de logger2
Ceci est un paramètre
f2
Fin de logger2