ProPython
Notions Avancées - Les Décorateurs
29 Mar, 2021

Notions Avancées - Les Décorateurs

Cet article est le second à propos des notions avancées. Si vous avez raté le premier dans lequel je parle des compréhensions, des itérateurs et des générateurs, vous le trouverez ici : Notions avancées - Compréhensions, Itérateurs et Générateurs.

Nous allons découvrir les décorateurs, qui sont en fait des fonctions modifiant le comportement d'autres fonctions, ensuite l'héritage multiple qui permet à une classe d'hériter de plusieurs classes, et finalement les interfaces, qui permettent de définir un prototype général dont peuvent hériter des sous classes.

Les Décorateurs

Définition

En Python, tout est objet, donc les fonctions sont des objets aussi et peuvent être passées en paramètres d'autres fonctions.

Un décorateur est une fonction prenant en paramètre une autre fonction, ce qui permet de modifier son comportement. Ils permettent par exemple d'enregistrer des logs, de vérifier qu'un utilisateur est bien connecté avant d'exécuter une fonction, de déclarer des méthodes ayant un comportement spécial dans une classe, etc...

Créer un décorateur

Nous avons dit qu'un décorateur est une fonction prenant en paramètre une autre fonction. Nous savons donc déjà en créer ! Nous allons créer des fonctions pour parler à une personne prenant en paramètre son prénom. Et nous allons créer une fonction générique prenant en paramètre une fonction (qui sera finalement l'une de nos fonctions pour parler), et renvoyant cette fonction avec le prénom "Charles" en paramètre.

def dire_bonjour(prenom):  # Première fonction pour parler
    print("Bonjour " + prenom)

def dire_au_revoir(prenom):  # Seconde fonction pour parler
    print("Au revoir " + prenom)

def parler_a_charles(parler):  # Décorateur
    return parler("Charles")


parler_a_charles(dire_bonjour)
# Bonjour Charles
parler_a_charles(dire_au_revoir)
# Au revoir Charles

Voici donc un premier exemple de décorateur ! Il est important de bien comprendre cet exemple pour la suite, puisqu'on va augmenter la complexité !

Déclarer des fonctions intérieures

Des fonctions intérieures sont des fonctions déclarées à l'intérieur d'autres fonctions. Ces fonctions étant définies dans un contexte (qui est la fonction parente), elles ne sont accessibles que dans ce contexte, donc essayer de les appeler depuis l'extérieur de la fonction vous générera une erreur. Voici un exemple de fonctions intérieures.

def parent():

    def first_child():  # On déclare une première fonction intérieure
        print("Première fonction !")
    def second_child():  # Puis une deuxième
        print("Deuxième fonction !")

    first_child()  # On appelle les fonctions définies ci-dessus
    second_child()

parent()
# Première fonction !
# Deuxième fonction !

Dans notre exemple, on se contente d'appeler les deux fonctions, et on ne renvoie rien. On aurait pu également renvoyer l'une des deux fonctions, en fonction d'un paramètre, comme ceci :

def parent(num):

    def first_child():
        print("Première fonction !")
    def second_child():
        print("Deuxième fonction !")

    if num == 1:
        return first_child  # On renvoie une fonction !
    else:
        return second_child

parent(1)
# Rien ne s'affiche

Cependant en exécutant ce code, rien ne s'affiche.

Pourtant on renvoie la première fonction, elle devrait donc s'exécuter et afficher "Première fonction !" ?

Et bien non, on n'exécute pas la première fonction. En fait, on se contente de la renvoyer sans l'appeler. Si vous faites print(parent(1)), vous obtiendrez quelque chose comme ceci : <function parent.<locals>.first_child at 0x00000238B52C9160>. Cela signifie que parent(1) est en fait une fonction et non pas l'appel à une fonction, ce qui est logique puisque dans le code ci-dessus, on renvoie les fonctions sans les appeler. Pour les appeler, il faut donc utiliser les parenthèses () comme pour toute fonction. Ainsi, on peut soit écrire ceci : return first_child() au lieu de return first_child, soit ceci : parent(1)(). Et oui, puisque parent(1) est une fonction, parent(1)() est l'appel à cette fonction !

Décorateurs simples

On a maintenant tous les outils pour créer un décorateur basique.

Voici donc un exemple :

def mon_decorateur(fonction):
    def inner():
        print("Exécution de la fonction ", fonction)
        fonction()
        print("La fonction ", fonction, " s'est exécutée avec succès")
    return inner


def dire_bonjour():
    print("Bonjour !")
dire_bonjour = mon_decorateur(dire_bonjour)

dire_bonjour()
# Exécution de la fonction  <function dire_bonjour at 0x000001EEB94F9820>
# Bonjour
# La fonction  <function dire_bonjour at 0x000001EEB94F9820>  s'est exécutée avec succès

Ici, ce qu'il faut remarquer est la ligne suivante : dire_bonjour = mon_decorateur(dire_bonjour). En fait, mon_decorateur(dire_bonjour) renvoie une fonction : la fonction dire_bonjour mais modifiée par notre décorateur. On associe la fonction renvoyée par notre décorateur à dire_bonjour ce qui permet donc de modifier cette fonction en la remplaçant par la fonction modifiée. Ainsi dire_bonjour() ne s'exécutera pas telle qu'on l'a premièrement définie, mais telle qu'elle est après avoir été modifiée par mon_decorateur. On dit donc que dire_bonjour est décorée par mon_decorateur.

Une fois que vous aurez compris cet exemple, je vous propose de réaliser un petit exercice pour vous entraîner. Nous allons réaliser un petit système d'authentification très simplifié. Nous allons donc créer deux fonctions permettant de récupérer des données d'un utilisateur selon un critère. Pour cela, on crée d'abord notre classe utilisateur :

class User:
    def __init__(self, id, money, code):
        self.id = id
        self.money = money
        self.code = code

On crée ensuite nos fonctions pour accéder aux données d'un utilisateur :

def get_money(user):
    print(user.money)
def get_code(user):
    print(user.code)

Maintenant, à vous de jouer ! L'objectif est de décorer les deux fonctions ci-dessus pour qu'elles ne renvoient les données que si l'ID de l'utilisateur est égal à 1, sinon elles affichent "Accès refusé".

Correction

Voilà, si vous avez réussi l'exercice, parfait, sinon ce n'est pas grave, relisez la correction plusieurs fois et vous finirez par comprendre, les décorateurs sont une notion assez complexe !

Meilleure syntaxe

La syntaxe vue précédemment est bien pour comprendre le fonctionnement des décorateurs. En revanche, elle n'est pas très pratique et Python fournit une meilleure syntaxe, plus lisible, pour décorer des fonctions. Reprenons notre premier exemple :

def mon_decorateur(fonction):
    def inner():
        print("Exécution de la fonction ", fonction)
        fonction()
        print("La fonction ", fonction, " s'est exécutée avec succès")
    return inner

@mon_decorateur  # Cette syntaxe remplace dire_bonjour = mon_decorateur(dire_bonjour)
def dire_bonjour():
    print("Bonjour !")

dire_bonjour()
...

On utilise donc le symbole @ pour décorer une fonction, cela remplace l'autre syntaxe.

Décorateurs de fonctions à paramètres

Imaginez que vous souhaitez décorer une fonction à paramètres. Si vous la décorez comme on l'a déjà fait ci-dessus, cela va en fait figer la fonction, puisque les décorateurs d'au dessus n'acceptaient pas de paramètres. Par exemple, dans le petit exercice que je vous avez proposé, lorsqu'on a décoré get_money, cela l'a figée avec les données de l'utilisateur. Maintenant, que faire si on souhaite par exemple qu'un utilisateur soit administrateur et puisse accéder aux données de chaque utilisateur. On ne peut pas avec notre définition précédente, car get_money est figée pour un unique utilisateur. Voici donc comment redéfinir get_money de telle sorte qu'elle prenne en paramètres deux utilisateurs, le premier est celui qui souhaite accéder aux données du deuxième, et il ne peut y accéder que si son id est 1.

def securize(operation):

    def inner(user, other_user):
        def refuse():
            print("Action refusée")

        def execute():
            return operation(other_user)

        if user.id != 1:
            return refuse()
        return execute()

    return inner

@securize
def get_money(user):
    print(user.money)

get_money(user, user2)

La forme de notre décorateur est un peu différente ici. On enveloppe tout notre code dans une fonction inner à laquelle on passe des paramètres, qui sont les paramètres qu'on passe lorsqu'on appelle notre fonction décorée. On passe donc nos deux utilisateurs en paramètres. Ensuite, on passe en paramètre à la fonction execute l'utilisateur dont on souhaite obtenir les données, donc le deuxième utilisateur. Puis finalement, on retourne l'exécution de la fonction correspondante en fonction de l'id de l'utilisateur qui souhaite accéder aux données. L'exemple est un peu compliqué j'avoue, relisez le plusieurs fois et essayer d'expérimenter par vous-même afin de le comprendre !

Décorateurs chaînés

Est-il possible de décorer une fonction avec deux décorateurs ? Bien-sûr, on peut même la décorer avec autant de décorateurs qu'on veut en fait. Voici la syntaxe pour utiliser plusieurs décorateurs.

@deco1
@deco2
def ma_function():
    ...

Ici, il faut faire attention à l'ordre dans lequel on décore la fonction. Celui tout en haut, appellera celui en dessous, qui appellera celui en dessous, etc... jusqu'à arriver à la fonction. Voici un exemple pour comprendre :

def deco1(func):
    print("Deco 1")
    return func
def deco2(func):
    print("Deco 2")
    return func

@deco1
@deco2
def dire_bonjour():
    print("Bonjour !")

dire_bonjour()
# Deco 2
# Deco 1
# Bonjour

L'ordre est assez complexe à comprendre. En fait, c'est comme si on appelait deco1(deco2(dire_bonjour)). deco2(dire_bonjour) affiche "Deco 2" et renvoie la fonction dire_bonjour, qui est donc passée en paramètre à deco1 qui affiche "Deco 1" et renvoie la fonction dire_bonjour qu'on exécute ensuite. Voilà pourquoi l'ordre est si particulier.

Décorateurs à arguments

On peut également passer des arguments aux décorateurs directement. Par exemple, imaginez un décorateur repeat qui répète une fonction x fois. x est donc un paramètre du décorateur lui-même, et non pas de la fonction décorée. Voici comment coder ce décorateur :

def repeat(x):
    def deco_repeat(func):
        def wrap():
            for i in range(x):
                func()
        return wrap
    return deco_repeat

@repeat(3)
def dire_bonjour():
    print("Bonjour !")

dire_bonjour()
# Bonjour !
# Bonjour !
# Bonjour !

La syntaxe peut paraître un peu tordue, mais en fait elle reprend ce que nous avons déjà vu, nous rajoutons juste une couche.

Décorateurs à mémoire

Il peut être utile de réaliser des décorateurs pouvant garder un état qui évolue avec les différents appels. Par exemple, si on veut réaliser un décorateur qui compte le nombre de fois qu'une fonction est appelée. Pour cela, on va avoir besoin des attributs de fonctions. Ce sont simplement des attributs associés à des fonctions, par exemple :

def a():
    a.count += 1
    return a.count

a.count = 0
print(a())
print(a())
print(a())

# 1 
# 2
# 3

Leur utilisation est similaire à celle des attributs de classes. Maintenant que nous connaissons ceci, essayons d'implémenter le décorateur pour compter le nombre d'appels d'une fonction :

def count(func):

    def inner():
        inner.count += 1
        print("Count : ", inner.count)
        return func()
    inner.count = 0

    return inner

@count
def dire_bonjour():
    print("Bonjour !")

dire_bonjour()
dire_bonjour()

# Count :  1
# Bonjour !
# Count :  2
# Bonjour !
Mais on réinitialise inner.count à chaque appel, pourquoi ça s'incrémente quand même ?

En fait non, on ne réinitialise pas inner.count à chaque appel, puisque l'attribut n'est associé que lors de la création du décorateur. inner.count = 0 n'est donc exécuté qu'une seule fois. En effet, rappelez vous de la première syntaxe pour créer un décorateur : dire_bonjour = count(dire_bonjour). Donc, l'attribut n'est déclaré qu'une fois puisque tout le code dans le décorateur ne s'exécute qu'une fois, à sa création.

Le mot de la fin

Les décorateurs sont une notion assez compliquée, mais également très utile en Python. Il est important de bien les comprendre, du moins les usages basiques. Nous pourrions également aller plus loin, on peut par exemple créer des classes agissant comme des décorateurs, ou bien utiliser des paramètres non nommés afin d'augmenter la flexibilité des décorateurs, mais cela devient très spécifique et pas forcément utile pour tout le monde.

Si vous avez des questions, ou que vous avez besoin de précision sur les exemples utilisés ou sur cette notion, n'hésitez-pas à me contacter par mail ou via la section intégrée au site ! On se retrouve plus tard pour la suite de votre apprentissage du Python !

Laisser un commentaire

Premium - 15€/mois

L'accès à des articles inédits, à une multitude de ressources, à de nouveaux projets, mais également à des vidéos explicatives, découvrez ici pourquoi passer premium.

Articles liés

Catégories

Ressources

Retrouvez une collection de ressources (des scripts, des fiches résumé, des images...) liées aux articles du blog ou au Python.
Voir

Contact

contact@propython.fr
Se connecter pour envoyer un message directement depuis le site.

Navigation

AccueilSe connecterCréer un compteRessourcesPremium

Catégories

Pages légales

Politique de confidentialitéMentions légalesConditions générales de vente