Les décorateurs Python pour modifier les fonctions

Les décorateurs Python sont des modèles qui ajoutent de nouvelles fonctionnalités à un objet déjà existant sans avoir besoin de modifier sa structure. Le décorateur est appelé avant la fonction à modifier.

On l’utilise pour modifier la fonctionnalité d’une méthode, d’une fonction ou d’une classe dynamiquement sans avoir besoin de modifier l’objet lui-même.

Pour comprendre les décorateurs, commencez par comprendre comment Python permet de définir des fonctions imbriquées, c’est-à-dire des fonctions à l’intérieur d’autres fonctions.

décorateurs Python

Les fonctions imbriquées

def externe(n):
    print("J'englobe la fonction interne")
    def interne():
        print("J'hérite de la fonction englobante le", n)
    interne()

externe(5)

L’exécution du code.

J'englobe la fonction interne
J'hérite de la fonction englobante le 5

Comme vous pouvez le constater, on a appelé la fonction interne à l’intérieur de la fonction externe. En fait, lorsque nous avons appelé la fonction externe, la fonction interne a également été exécutée. De plus, la fonction interne a accédé aux paramètres de la fonction externe.

Affecter une fonction à un nom

En Python, les fonctions sont également des objets comme les autres. Par conséquent, elles peuvent être affectées à des noms, puis renvoyées par d’autres fonctions.
La différence entre les objets de données et les fonctions est que les fonctions peuvent être appelées.
Par exemple, nous pouvons assigner la fonction nommée externe à un nom fonc puis l’appeler avec ce nom :

fonc = interne    # Le nom 'fonc' est un alias de 'interne'.
fonc()            # Nous appelons 'interne' 'avec son alias.

Maintenant, les choses vont devenir plus intéressantes. Nous allons toujours définir la fonction externe avec la fonction interne. Mais cette fois, avec une nouvelle fonctionnalité. Au lieu d’appeler la fonction interne, nous allons renvoyer sa valeur dans la fonction externe avec un return, puis utiliser la technique d’affectation précédente.

def externe(n):
    def interne():
        print("J'hérite de la fonction englobante le", n)
    return interne

externe(5)    # rien ne sera exécuté
fonc = externe(10)
fonc()

La surprise ici est que l’exécution de externe, ne semble rien faire.
Mais en fait, externe a fait quelque chose d’important. Elle a créé une nouvelle fonction en lui passant un argument, puis a renvoyé la valeur de cette nouvelle fonction.
La nouvelle fonction a été affectée au nom fonc. Ce nom peut donc être utilisé pour appeler la fonction nouvellement définie.

Une fonction comme argument

Maintenant, il reste une chose à ajouter, et nous seront prêts à créer des décorateurs Python. Une fonction peut à la fois retourner une fonction et prendre une fonction comme argument. Vous pouvez donc écrire ceci :

def externe(fonc):
    def interne():
        print("Je suis avant la fonction")
        fonc()
        print("Je suis après la fonction")
    return interne    # "externe" renvoie une nouvelle fonction!

L’objectif de la fonction externe est maintenant de prendre une fonction fonc comme argument, elle sera utilisée pour construire la nouvelle fonction qui sera renvoyée. Par exemple, définissons une fonction bonjour.

def bonjour():
    print('Bonjour')

Maintenant nous allons passer la fonction bonjour() à externe(), qui retournera une nouvelle fonction qui fait tout ce que bonjour() a fait, mais elle en fait plus. Le résultat de la nouvelle fonction est affecté au nom new.

new = externe(bonjour)

Maintenant, new est un alias pour la fonction nouvellement créée. Vous pouvez utiliser new pour appeler cette fonction aussi souvent que vous le souhaitez.

new()

L’exécution du code.

Je suis avant la fonction
Bonjour
Je suis après la fonction

Voici un autre exemple. Nous pouvons utiliser externe() pour englober n’importe quelle autre fonction, puis affecter le résultat au nom new qui est le nom de la nouvelle fonction, et nous pouvons ainsi l’appeler.

def print_nums():
    for i in [1, 2, 3]:
        print(i, end=' ')
    print()

new = externe(print_nums)

new()

L’exécution du code.

Je suis avant la fonction
1 2 3 
Je suis après la fonction

Nous avons créer une nouvelle fonction autour de print_nums, puis nous l’avons affecté à new.
C’est le concept d’une fonction encapsulée.

Toute fonction que nous encapsulons fera ce qu’elle faisait auparavant, mais elle affichera également un message avant et après l’exécution. Nous pouvons même réattribuer l’ancien nom de la fonction ! Voici un exemple :

print_nums = externe(print_nums)

print_nums()

D’accord, c’est bizarre. Le nom de la fonction, print_num, est-il réaffecté à une nouvelle version de … lui-même ? Oui, c’est exactement ce qui se passe.

Vous aurez le même résultat.

Si nous résumons ce concept en un code, nous aurons.

def externe(fonc):
    # créer une nouvelle fonction
    def interne():
        print(message 1)
        fonc()
        print(message 2)
    return interne
    
# la nouvelle fonction remplace l'ancienne    
fonc = externe(fonc)

Le point clé ici est que le nom fonc, qui au début faisait référence à une fonction simple, se réfère maintenant à une nouvelle version, qui est améliorée.

Créer un décorateur Python

Un décorateur Python réaffecte un nom de fonction à une version encapsulée de la fonction d’origine. En fait, ces concepts sont utiles pour le débogage et les tests de performance. Reprenons l’exemple précédent.

def mon_decorateur(ma_fonction):
    # Prendre une fonction normaL.
    # Qui servira à créer une nouvelle version.
    # Renvoyez cette nouvelle fonction
    def nouvelle_fonction():
        print('Je fais des choses supplémentaires.')
        ma_fonction()
        print("Je fais d'autres choses supplémentaires.")
    return nouvelle_fonction

Donc, le décorateur crée une nouvelle fonction…et la renvoie. Nous l’attribuons au nom de la fonction d’origine. Nous pouvons faire la même chose pour toutes les fonctions que nous choisissons !

fonc1 = mon_decorateur(fonc1)  # encapsuler fonc1
fonc2 = mon_decorateur(fonc2)  # encapsuler fonc2
fonc3 = mon_decorateur(fonc3)  # encapsuler fonc3

La dernière étape consiste à appliquer la syntaxe de décoration. Cette syntaxe génère une ligne similaire à chacune des trois lignes ci-dessus.

@mon_decorateur
def bonjour():
    print('Bonjour!')

En fait, Python traduit cela en :

def bonjour():
    print('Bonjour!')

bonjour = mon_decorateur(bonjour)

Voici la syntaxe qui résume le fonctionnement des décorateurs Python :

@mon_decorateur
def fonc(args):
    instructions

Ce qui équivaut le code suivant :

def fonc(args):
    instructions
fonc = mon_decorateur(fonc)

Les décorateurs avec des arguments

Comment faire si nous voulons décorer une fonction avec des arguments ?
Et si elle nous renvoie une valeur ?
Le premier changement consiste à passer la liste d’arguments à la fonction encapsulée en utilisant * args et ** kwargs.

def mon_decorateur(ma_fonction):
    def nouvelle_fonction(*args, **kwargs):
        print('Je fais des choses supplémentaires.')
        ma_fonction(*args, **kwargs)
        print("Je fais d'autres choses supplémentaires.")
    return nouvelle_fonction

L’autre changement consiste à faire en sorte que nouvelle_fonction() renvoie la valeur de ma_fonction().

def mon_decorateur(ma_fonction):
    def nouvelle_fonction(*args, **kwargs):
        print('Je fais des choses supplémentaires.')
        valeur = ma_fonction(*args, **kwargs)
        print("Je fais d'autres choses supplémentaires.")
        return valeur
    return nouvelle_fonction

Maintenant, la valeur renvoyée par la fonction ma_fonction(), est également renvoyée par nouvelle_fonction().
Si ma_fonction() n’a pas de valeur à renvoyer, None sera renvoyée par défaut.

Le code qui correspond à un décorateur avec des arguments est :

def mon_decorateur(ma_fonction):
    def nouvelle_fonction(*args):
        # Créer une nouvelle fonction
        print(message 1)
        valeur = ma_fonction(*args)
        print(message 2)
        return valeur
    return nouvelle_fonction

@mon_decorateur
def fonc(args...)
    instructions

Prenons un exemple simple pour que vous puissiez assimiler.
Nous avons une fonction somme qui nous retourne la somme de deux entiers.

def somme(x,y):
    return x+y

Pour une raison ou une autre, nous voulons multiplier la valeur retournée par 2, sans avoir besoin de modifier la fonction somme.

Nous allons donc appliquer ce que nous avons appris avec les décorateurs Python.

def deco(fonc):
    def nouvelle(x,y):
        val = fonc(x,y)* 2
        return val
    return nouvelle

somme = deco(somme)

print(somme(2,3))

Si nous appliquons la syntaxe des décorateurs Python :

@deco
def somme(x,y):
    return x+y

print(somme(2,3))

L’exécution du code.

10

Aussi, sachez qu’il est possible d’appliquer plusieurs décorateurs à une seule fonction.

Les décorateurs Python pour modifier les fonctions

2 commentaires sur “Les décorateurs Python pour modifier les fonctions

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée.

Retour en haut