Historique de la question
Les décorateurs sont l'un des outils les plus puissants de Python, permettant de modifier le comportement des fonctions ou des méthodes. Parfois, il est nécessaire de "cercler" non seulement la fonction, mais de configurer le décorateur à l'aide de paramètres (arguments). Ces cas surviennent lors de la journalisation, lors de vérifications de temps, de limitations d'accès, etc.
Problème
Les décorateurs ordinaires ne prennent qu'une seule fonction à encadrer. Lorsque des paramètres doivent être transmis au décorateur, la syntaxe devient plus complexe, ce qui conduit souvent à des erreurs, surtout lors de la nesting de fonctions et du passage de *args/**kwargs.
Solution
Un décorateur avec des paramètres est réalisé en tant que fonction de plus haut ordre : d'abord, la fonction "décorante" externe est appelée avec des arguments, qui crée et retourne le propre décorateur, et le décorateur reçoit ensuite la fonction et retourne l'enveloppe :
def repeat(n): def decorator(func): def wrapper(*args, **kwargs): result = None for _ in range(n): result = func(*args, **kwargs) return result return wrapper return decorator @repeat(3) def greet(name): print(f"Hello, {name}!") greet("Python") # Sortie : Hello, Python! (3 fois)
Caractéristiques clés :
Pourquoi ne peut-on pas réaliser un décorateur avec des paramètres de la même manière qu'un décorateur ordinaire ?
Si vous utilisez @decorator, Python passe la fonction comme argument au décorateur. Si vous ajoutez des parenthèses (@decorator()), Python appelle d'abord la fonction, et seul son résultat est interprété comme un décorateur.
def deco(func): # décorateur ordinaire : @deco def deco_with_args(arg): # décorateur avec argument : @deco_with_args(arg)
En quoi les décorateurs avec des arguments diffèrent-ils fondamentalement des décorateurs sans arguments au niveau de l'appel ?
Les décorateurs sans arguments reçoivent en entrée une fonction, tandis que les décorateurs avec des arguments ne reçoivent pas une fonction, mais des paramètres, et retournent eux-mêmes un décorateur.
Comment utiliser correctement functools.wraps et pourquoi le faire ?
functools.wraps(func) dans le wrapper conserve le nom, la chaîne de documentation et d'autres métadonnées de la fonction originale, sinon toutes ces informations seront remplacées par le wrapper, ce qui complique le débogage et l'introspection.
import functools def deco_with_args(arg): def decorator(func): @functools.wraps(func) def wrapper(*args, **kwargs): return func(*args, **kwargs) return wrapper return decorator
Un décorateur a été réalisé sans tenir compte des paramètres ou avec un nombre incorrect de fonctions imbriquées :
def log(level): def wrapper(func): # erreur - le wrapper devrait être plus profond print(f"Log: {level}") func() # La fonction n'est pas retournée comme décorateur return wrapper @log("INFO") def action(): print("Work!")
Avantages :
Inconvénients :
Utilisation de functools.wraps et de fonctions imbriquées correctes :
import functools def timer(units): def decorator(func): @functools.wraps(func) def wrapper(*args, **kwargs): import time start = time.time() result = func(*args, **kwargs) end = time.time() if units == 'ms': duration = (end - start) * 1000 else: duration = end - start print(f"Duration: {duration:.4f} {units}") return result return wrapper return decorator @timer('ms') def op(): sum(range(1000)) op()
Avantages :
Inconvénients :