Parcours
/
Python
Python
/
Programme
/
Décorateurs
Dé

Décorateurs en Python

{one: "%{count} exercice", many: "%{count} d'exercices", other: "%{count} exercices"}

À propos de Décorateurs

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.

Un décorateur est une fonction d'ordre supérieur

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.

À quoi ressemble l'utilisation d'un décorateur

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)

Écris un décorateur simple

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.

Les fonctions internes

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.

Un décorateur de validation qui utilise une fonction interne

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.

Décore une fonction qui prend un nombre arbitraire d'arguments

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.

Les décorateurs qui ont leurs propres paramètres

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.

Modifie via GitHub Le lien s'ouvre dans une nouvelle fenêtre ou un nouvel onglet