Les décorateurs sont des fonctions qui prennent une autre fonction en argument dans le but d'étendre ou de remplacer le comportement de la fonction qui leur est passée.
Si la fonction A est un décorateur et que la fonction B est son argument, alors la fonction A modifie, étend ou remplace le comportement de la fonction B sans modifier le code de la fonction B.
On dit que la fonction décoratrice A enveloppe la fonction B.
Même si l'on parle de « modifier » le comportement, la fonction enveloppée n'est pas réellement modifiée.
Le comportement est soit ajouté autour de la fonction enveloppée (et de ce qu'elle renvoie), soit le comportement de la fonction enveloppée est remplacé par un autre comportement.
Une fonction d'ordre supérieur est une fonction qui accepte une ou plusieurs fonctions en arguments et/ou qui en renvoie une ou plusieurs. Une fonction, utilisée comme argument ou renvoyée par une autre fonction, est une fonction de première classe. Une fonction Python, en tant qu'objet appelable, est une fonction de première classe que l'on peut stocker dans une variable ou utiliser comme argument, comme n'importe quelle autre valeur ou objet. Les fonctions d'ordre supérieur et les fonctions de première classe se combinent pour rendre les décorateurs possibles.
Le symbole @ est placé devant le nom de la fonction décoratrice et juste au-dessus de la fonction à décorer, comme ceci :
@decorator
def decorated_function():
pass
Certains décorateurs acceptent des arguments :
@decorator_with_arg(name="Bob")
def decorated_function2():
pass
Si un décorateur a défini des arguments par défaut, tu dois utiliser des parenthèses dans l'appel @decorator() pour qu'il fonctionne, comme tu le ferais pour appeler n'importe quelle fonction :
@decorator_with_default_arg()
def decorated_function3():
pass
Si un décorateur prend des arguments positionnels, ne pas fournir ces arguments provoquera une erreur qui ressemblera à quelque chose comme :
TypeError: decorator_with_pos_arg() missing 1 required positional argument: 'name'
La syntaxe @decorator est du sucre syntaxique, autrement dit une forme abrégée pour appeler la fonction décoratrice et lui passer la fonction décorée en argument.
Voici des exemples de manières alternatives d'appeler un décorateur :
def function():
pass
function = decorator(function)
def function2():
pass
function2 = decorator_with_arg(name="Bob")(function2)
def function3():
pass
function3 = decorator_with_default_arg()(function3)
La plupart des décorateurs ont pour but d'étendre ou de remplacer le comportement d'une autre fonction, mais certains décorateurs peuvent se contenter de renvoyer les fonctions qu'ils enveloppent.
Les décorateurs sont des fonctions qui prennent au moins un argument : la fonction qu'ils enveloppent. Ils renvoient généralement soit la fonction enveloppée, soit le résultat d'une expression qui utilise cette fonction enveloppée.
Un décorateur simple, qui se contente de renvoyer la fonction qu'il enveloppe, peut s'écrire comme suit :
>>> def do_nothing(func):
... return func
...
... @do_nothing
... def function4():
... return 4
...
>>> print(function4())
4
Un décorateur peut se contenter d'ajouter des effets de bord, comme des informations supplémentaires destinées à la journalisation :
>>> def my_logger(func):
... print(f"Entering {func.__name__}")
... return func
...
... @my_logger
... def my_func():
... print("Hello")
...
>>> my_func()
Entering my_func
Hello
Un décorateur ne se renvoie pas lui-même. Il peut renvoyer ses arguments de fonction, une autre fonction, ou une ou plusieurs valeurs qui remplacent la valeur de retour de la fonction reçue ou décorée. Si un décorateur renvoie une autre fonction, il renvoie généralement une fonction interne.
Une fonction peut être définie à l'intérieur d'une autre fonction. Une telle fonction imbriquée s'appelle une fonction interne. Un décorateur peut utiliser une fonction interne pour envelopper son argument de fonction. Le décorateur renvoie alors sa fonction interne. La fonction interne peut ensuite renvoyer l'argument de fonction d'origine.
Voici un exemple de décorateur utilisé pour la validation :
def my_validator(func):
def my_wrapper(world):
print(f"Entering {func.__name__} with {world} argument")
if world == "Pluto":
print("Pluto is not a planet!")
else:
return func(world)
return my_wrapper
@my_validator
def my_func(planet):
print(f"Hello, {planet}!")
my_func("World")
#-> Entering my_func with World argument
#-> Hello, World!
my_func("Pluto")
#-> Entering my_func with Pluto argument
#-> Pluto is not a planet!
Sur la première ligne, on a la définition de l'argument func du décorateur.
Sur la ligne suivante se trouve la définition de la fonction interne du décorateur, qui enveloppe l'argument func.
Comme la fonction interne enveloppe l'argument func du décorateur, elle reçoit le même argument que celui passé à func.
Remarque que la fonction d'enveloppe n'est pas obligée d'utiliser le même nom pour l'argument que celui défini dans func.
La fonction d'origine utilise planet et le décorateur utilise world, et le décorateur fonctionne quand même.
La fonction interne renvoie soit func, soit, si world == "Pluto", affiche que Pluton n'est pas une planète.
Elle pourrait à la place être écrite pour lever une ValueError.
Ainsi, la fonction interne enveloppe func et renvoie soit func, soit autre chose qui remplace ce que ferait func.
Le décorateur renvoie sa fonction interne.
La fonction interne peut ou non renvoyer la fonction d'origine qui lui a été passée.
Selon le code qui s'exécute conditionnellement dans la fonction d'enveloppe ou la fonction interne, func peut être renvoyée, une erreur peut être levée, ou une valeur du type de retour de func peut être renvoyée.
On peut écrire des décorateurs pour des fonctions qui prennent un nombre arbitraire d'arguments en utilisant la syntaxe *args et **kwargs.
Voici un exemple de décorateur pour une fonction qui prend un nombre arbitraire d'arguments :
def double(func):
def wrapper(*args, **kwargs):
return func(*args, **kwargs) * 2
return wrapper
@double
def add(*args):
return sum(args)
print(add(2, 3, 4))
#-> 18
print(add(2, 3, 4, 5, 6))
#-> 40
Ceci fonctionne pour doubler la valeur de retour de la fonction passée en argument. Si on veut tripler, quadrupler, etc. la valeur de retour, on peut ajouter un paramètre au décorateur lui-même, comme on le montre dans la section suivante.
Voici un exemple de décorateur que l'on peut configurer pour multiplier la valeur de retour de la fonction décorée par un montant arbitraire :
def multi(factor=1):
if factor == 0:
raise ValueError("factor must not be 0")
def outer_wrapper(func):
def inner_wrapper(*args, **kwargs):
return func(*args, **kwargs) * factor
return inner_wrapper
return outer_wrapper
@multi(factor=3)
def add(*args):
return sum(args)
print(add(2, 3, 4))
#-> 27
print(add(2, 3, 4, 5, 6))
#-> 60
Les premières lignes vérifient que factor n'est pas 0.
Ensuite, la fonction d'enveloppe externe est définie.
Elle a la même signature que celle que l'on attend d'un décorateur sans paramètre.
La fonction d'enveloppe externe contient une fonction interne avec la même signature que la fonction d'origine.
La fonction d'enveloppe interne se charge de multiplier la valeur renvoyée par la fonction d'origine par l'argument passé au décorateur.
La fonction d'enveloppe externe renvoie la fonction d'enveloppe interne, et le décorateur renvoie la fonction d'enveloppe externe.
Voici un exemple de décorateur paramétré qui contrôle s'il valide ou non l'argument passé à la fonction d'origine :
def check_for_pluto(check=True):
def my_validator(func):
def my_wrapper(world):
print(f"Entering {func.__name__} with {world} argument")
if check and world == "Pluto":
print("Pluto is not a planet!")
else:
return func(world)
return my_wrapper
return my_validator
@check_for_pluto(check=False)
def my_func(planet):
print(f"Hello, {planet}!")
my_func("World")
#-> Entering my_func with World argument
#-> Hello, World!
my_func("Pluto")
#-> Entering my_func with Pluto argument
#-> Hello, Pluto!
Ceci permet de basculer facilement entre vérifier Pluto ou non, et ce sans avoir à modifier my_func.