Écrire proprement des décorateurs avec la fonction wraps
- 2021-08-31
- Publié par : Christophe DELEUZE
- Catégorie : Python
Le sujet de cet article est de vous permettre d’écrire proprement des décorateurs avec la méthode wraps() du module functools.
functools est un module Python standard pour les fonctions d’ordre supérieur (fonctions qui agissent sur d’autres fonctions ou qui renvoient d’autres fonctions). Parmi les fonctions de ce module, celle qui nous intéresse aujourd’hui est wraps(). wraps() est un décorateur qui s’applique à la fonction wrapper d’un décorateur. Il met à jour la fonction wrapper pour qu’elle ressemble à une fonction wrapped en copiant des attributs tels que __name__, __doc__(la docstring), etc.
Première approche sans functools.wraps()
Avant d’utiliser de wraps(), attardons-nous sur un décorateur classique :
def decorateur(func):
def wrapper(*args, **kwargs):
"""Docstring de la Fonction Wrapper"""
# Pre-process
# ...
# Appel de la fonction
func()
# Post-process
# ...
return wrapper
@decorateur
def premiere_fonction():
"""Docstring de la Premiere Fonction"""
print("Premiere Fonction")
@decorateur
def seconde_fonction(a):
"""Docstring de la Seconde Fonction"""
print("Seconde Fonction")
print(premiere_fonction.__name__)
print(premiere_fonction.__doc__)
print(seconde_fonction.__name__)
print(seconde_fonction.__doc__)
La sortie sera la suivante :
wrapper
Docstring de la Fonction Wrapper
wrapper
Docstring de la Fonction Wrapper
Maintenant, que se passera-t-il si nous écrivons help(premiere_fonction) et help(seconde_fonction) :
print("-->Premiere Fonction")
help(premiere_fonction)
print("-->Seconde Fonction")
help(seconde_fonction)
Cela produira :
-->Premiere Fonction
Help on function wrapper in module __main__:
wrapper(*args, **kwargs)
Docstring de la Fonction Wrapper
-->Seconde Fonction
Help on function wrapper in module __main__:
wrapper(*args, **kwargs)
Docstring de la Fonction Wrapper
Alors que le code ci-dessus est parfaitement fonctionnel, prenez le temps de considérer ceci : si vous écrivez une API ou une bibliothèque et que quelqu’un veut savoir ce que fait votre fonction et son nom ou tapez simplement help(votre_fonction), il affichera toujours le nom et la docstring de la fonction wrapper. Cela deviendra encore plus confus si vous avez utilisé la même fonction wrapper pour différentes fonctions, car elle affichera les mêmes détails pour chacune d’entre elles.
Idéalement, il devrait afficher le nom et la docstring de la fonction encapsulée au lieu de la fonction qui encapsule. La solution manuelle serait de récupérer de la fonction encapsulée ses attributs __name__ et __doc__ afin de remplacer ceux de la fonction l’encapsulant.
def decorateur(func):
def wrapper(*args, **kwargs):
"""Docstring de la Fonction Wrapper"""
# Pre-process
# ...
# Appel de la fonction
func()
# Post-process
# ...
wrapper.__name__ = func.__name__
wrapper.__doc__ = func.__doc__
return wrapper
@decorateur
def premiere_fonction():
"""Docstring de la Premiere Fonction"""
print("Premiere Fonction")
@decorateur
def seconde_fonction(a):
"""Docstring de la Seconde Fonction"""
print("Seconde Fonction")
print(premiere_fonction.__name__)
print(premiere_fonction.__doc__)
print(seconde_fonction.__name__)
print(seconde_fonction.__doc__)
Le résultat est maintenant le suivant :
premiere_fonction
Docstring de la Premiere Fonction
seconde_fonction
Docstring de la Seconde Fonction
Cela résout le problème, mais que se passe-t-il si nous tapons à nouveau help(votre_fonction) :
print("-->Premiere Fonction")
help(premiere_fonction)
print("-->Seconde Fonction")
help(seconde_fonction)
La sortie sera :
-->Première fonction
Help on function premiere_fonction in module __main__:
premiere_fonction(*args, **kwargs)
Docstring de la Premiere Fonction
-->Deuxième fonction
Help on function seconde_fonction in module __main__:
seconde_fonction(*args, **kwargs)
Docstring de la Seconde Fonction
Comme vous pouvez le voir, il y a toujours un problème, c’est-à-dire la signature de la fonction, il montre la signature utilisée par la fonction wrapper (ici, la signature générique) pour chacun d’eux. De plus, si vous implémentez de nombreux décorateurs, vous devez écrire ces lignes pour chacun d’entre eux.
Donc, pour gagner du temps et augmenter la lisibilité, nous allons utiliser la fonction wraps() du module functools comme décorateur pour la fonction wrapper et dont le rôle sera de mettre à jour toutes les informations à notre place.
Seconde approche avec functools.wraps()
Sur la base de l’exemple précédent, cette fois j’ajoute wraps() dans notre décorateur :
from functools import wraps
def decorateur(func):
@wraps(func)
def wrapper(*args, **kwargs):
"""Docstring de la Fonction Wrapper"""
# Pre-process
# ...
# Appel de la fonction
func()
# Post-process
# ...
return wrapper
@decorateur
def premiere_fonction():
"""Docstring de la Premiere Fonction"""
print("Premiere Fonction")
@decorateur
def seconde_fonction(a):
"""Docstring de la Seconde Fonction"""
print("Seconde Fonction")
print(premiere_fonction.__name__)
print(premiere_fonction.__doc__)
print(seconde_fonction.__name__)
print(seconde_fonction.__doc__)
Testons le nom et la docstring :
premiere_fonction
Docstring de la Premiere Fonction
seconde_fonction
Docstring de la Premiere Fonction
Maintenant, si nous tapons help(premiere_fonction) :
print("-->Premiere Fonction")
help(premiere_fonction)
print("-->Seconde Fonction")
help(seconde_fonction)
Cela produit :
-->Première fonction
Help on function premiere_fonction in module __main__:
premiere_fonction()
Docstring de la Premiere Fonction
-->Deuxième fonction
Help on function seconde_fonction in module __main__:
seconde_fonction(a)
Docstring de la Seconde Fonction
Et voilà ! Tout fonctionne parfaitement maintenant et nous avons le comportement attendu.
Le mot de la fin
Comme l’utilisation de wraps() influence fortement la documentation, son emploi trouve aussi une utilité significative quand il est couplé à des décorateurs personnalisés tels que ceux que vous pourriez utiliser conjointement à des frameworks basés sur des décorateurs tels que Celery et FastAPI.
Par exemple, le Framework FastAPI qui vient avec sa documentation auto-générée, utilise intensivement les signatures et les attributs des fonctions décorées. Sans wraps(), la documentation de l’API REST ressemblerait à la même chose que ce que vous avez lu au début de cet article.
Voilà, j’espère que vous aurez apprécié ce petit article court et simple, la fonction wraps() du module functools.
Si vous avez apprécié, n’hésitez pas à partager. Sinon les commentaires sont là pour vos remarques.
Salut ,
Je suis , débutant en programmation.
Je souhaite commencer avec python.
J’ai quelques notions de base.
Avec vous des offres à me proposer ?
Tutos, livres ou autres ressources.
Merci .
Bonjour Fabrice,
Oui tu peux commencer par ce pdf de 472 pages :
apprendre_python3_5.pdf
Un jour je publierai un cours sur python en accès sur mon site mais c’est en cours de rédaction et il y en a pour encore un bon moment. (Très long de rédiger les cours).
Si tu as des questions, ou si tu souhaites que j’aborde, via un article, un sujet en particulier n’hésites pas à me demander je le ferais avec plaisir.
Bonne journée à toi.
Christophe D