@Le Decorateur

Avez-vous déjà vu @property, @classmethod, @staticmethod ? Les décorateurs sont souvent utilisés, surtout en programmation orientée objet, grâce aux "copier-coller" d'exemples trouvés sur internet, mais il peut être intéressant de comprendre ce que c'est, comment cela fonctionne, pour pouvoir les utiliser au mieux, et pourquoi créer ses propres objets.

 

Un décorateur, c'est quoi ?

Il s’agit tout simplement d'une fonction qui prend en paramètre une autre fonction. Ce type d'objet est très utile, car il permet d'effectuer des actions sur la fonction décorée de manière autonome et factorisée. Il permet par exemple :

  • de contrôler les entrées de la fonction (les paramètres qui lui sont fournis) ;
  • de contrôler la sortie de la fonction ;
  • de modifier totalement le comportement de la fonction.

 

Les décorateurs natifs à Python

Python met déjà à disposition plusieurs décorateurs natifs. Voici un petit tour d'horizon rapide des 3 principaux décorateurs utilisés :

@property

Ce décorateur permet de s'abstraire des parenthèses lors de l'appel d'une fonction ou méthode de classe. C'est particulièrement utile, en programmation orientée objet, lorsque vous voulez faire passer une méthode, pour un attribut de classe de manière invisible.

class Couple:
    def __init__(self, a, b):
        self._a = a
        self._b = b

    @property
    def somme(self):
        return self._a + self._b

couple = Couple(5, 8)
couple.somme

# 13

 

@classmethod

Ce décorateur, propre aux développements d'objets, permet de transformer une méthode nécessitant l'initialisation de la classe en une méthode de classe pour être appelée sans nécessairement avoir initialisé la classe au préalable.

class Nombre12:
    __N = 12

    @classmethod
    def add_to_me(clf, nb):
        return clf.__N + nb

Nombre12.add_to_me(2)

# 14

 

@staticmethod

Ce dernier objet, permet de supprimer les paramètres implicites hérités par la classe de la méthode décorée. Ces méthodes ainsi décorées pourront donc être appelées sans initialisation de la classe et auront l'avantage d'être plus performantes, car seul les paramètres nécessaires lui seront envoyés.

class Couple:

    @staticmethod
    def add(a, b):
        return a + b

Couple.add(5, 6)

# 11

 

Créer son propre décorateur

Maintenant que l'on a vu les décorateurs natifs de Python, et comment on les utilise. Nous sommes prêt à construire notre propre décorateur. Tout de suite et sans plus attendre, voici un premier exemple de décorateur maison.

def mon_premier_decorateur(function):
    def wrapper(*args, **kwargs):
        print("---- actions avant execution ----")
        data = function(*args, **kwargs)
        print("---- actions apres execution ----")
        return data

    return wrapper


@mon_premier_decorateur
def add(a, b):
    print(a + b)


add(5, 6)

# ---- actions avant execution ----
# 11
# ---- actions apres execution ----

Et voilà ! Pour créer notre décorateur nous avons donc implémenté une fonction mon_premier_decorateur(). Dans cette dernière, nous avons créé une nouvelle fonction wrapper(), qui appelle la fonction d'origine. (la clause *args permet de transmettre les paramètres de position de la fonction, la clause **kwargs permet de transmettre les paramètres appelés via mot clef de la fonction).

Pour résumé, lorsque l'on appelle une fonction décorée par mon_premier_decorateur(), Python va d'abord lancer mon_premier_decorateur() en lui donnant en paramètre la fonction décorée. La fonction mon_premier_decorateur() lance ensuite la fonction wrapper() en récupérant les paramètres de la fonction décorée dans les clauses *args et **kwargs. Et enfin wrapper() va exécuter la fonction d'origine.

Dans notre exemple, le décorateur affiche, via un print(), un texte avant et après l’exécution de la fonction. Il est bien-sûr possible de mettre ce décorateur à toutes les fonctions dont vous avez besoin en rappelant @mon_premier_decorateur sur les fonctions désirées. 

@mon_premier_decorateur
def helloword():
    print("hello word")


helloword()

# ---- actions avant execution ----
# hello word
# ---- actions apres execution ----

 

Exemple

Voici quelques exemples de décorateurs custom, que vous pourrez modifier à votre guise.

 

Vérification des variables fournies

Grâce à cet exemple, vous pouvez vérifier le type des paramètres fournis. Ici, la fonction add() ne se joue uniquement si tous les paramètres sont des int. Sinon, elle renvoie un message sur la sortie standard.

def check(function):
    def wrapper(*args, **kwargs):
        if all(isinstance(x, int) for x in args + tuple(kwargs.values())):
            return function(*args, **kwargs)
        else:
            print('Tous les paramètres ne sont pas des int')
    return wrapper


@check
def add(a, b):
    return a + b

add(2, '3')

# Tous les paramètres ne sont pas des int

 

Décorateur qui log

Voici un exemple de décorateur qui va logger la fonction décorée lors de son appel, ainsi que les paramètres utilisés. Les logs se retrouveront dans le fichier cryptide_logger.log.

import logging


def create_logger():
    logger = logging.getLogger('cryptide_logger')
    logger.setLevel(logging.INFO)

    logfile = logging.FileHandler('cryptide_logger.log')
    fmt = '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
    formatter = logging.Formatter(fmt)
    logfile.setFormatter(formatter)
    logger.addHandler(logfile)
    return logger


logger = create_logger()


def logit(function):
    def wrapper(*args, **kwargs):
        logger.info(f"{function}({args},{kwargs})")
        return function(*args, **kwargs)

    return wrapper


@logit
def add(a, b):
    return a + b

 

Supprime le retour de la fonction

Dans ce dernier extrait le décorateur va permettre de jouer la fonction mais empêchera qu'elle retourne un résultat.

def justrun(function):
    def wrapper(*args, **kwargs):
        function(*args, **kwargs)

    return wrapper


@justrun
def sendmail(to="admin@cryptide.fr", subject="decorator"):
    print("Sending ... Success")
    return f'Le mail a bien été envoyé à {to}'


sendmail()

# None

 

Les décorateurs avancés

Il existe quelques pratiques un peu plus exotiques et avancés sur les décorateurs de Python.

Les paramètres pour le décorateur

Il est possible de donner des paramètres directement au décorateur. Mais pour cela il faudra entourer le décorateur d'une nouvelle fonction. Voici un exemple en modifiant le décorateur @check créé précédemment.

def checkargument(type):
    def check(function):
        def wrapper(*args, **kwargs):
            if all(isinstance(x, type) for x in args + tuple(kwargs.values())):
                return function(*args, **kwargs)
            else:
                print(f'Tous les paramètres ne sont pas des {type}')

        return wrapper

    return check


@checkargument(float)
def add(a, b):
    return a + b


add(2, '3')

# Tous les paramètres ne sont pas des <class 'float'>

@checkargument(int)
def add(a, b):
    return a + b


add(2, '3')

# Tous les paramètres ne sont pas des <class 'int'>

Dans cet exemple le décorateur @checkargument va vérifier si tous les paramètres donnés à la fonction sont du même type que l'argument donné au décorateur. Ici, on vérifie d'abord avec le type float puis int.

 

Garder les docstring de commentaire d'une fonction

Lorsque vous voulez commenter votre projet, une bonne pratique consiste à faire des docstrings juste après la définition de la fonction. Cela aura pour effet d’automatiquement indiquer à Python que cette chaîne de caractères sert de documentation, et ainsi elle pourrait être lue à travers la fonction help().

def add(a, b):
    """
    Cette fonction additionne deux entiers
    Args:
        a (int): Premier entier a additionner
        b (int): Second entier a additionner

    Returns:
        int : La somme entre a et b
    """
    return a + b


help(add)

# Help on function add in module __main__:
# 
# add(a, b)
#     Cette fonction additionne deux entiers
#     Args:
#         a (int): Premier entier a additionner
#         b (int): Second entier a additionner
#    
#     Returns:
#         int : La somme entre a et b

Mais le problème quand on utilise des décorateurs c'est qu'ils écrasent cette merveilleuse documentation.

@check
def add(a, b):
    """
    Cette fonction additionne deux entiers
    Args:
        a (int): Premier entier a additionner
        b (int): Second entier a additionner

    Returns:
        int : La somme entre a et b
    """
    return a + b


help(add)

# Help on function wrapper in module __main__:

# wrapper(*args, **kwargs)

Mais pas de problème Python a pensé a tout, il existe un autre décorateur (et oui encore) qui permet de conserver la documentation. Il s’agit de functools.wraps(). Il suffit de l'utiliser sur la fonction wrapper() (la fonction interne) de la manière suivante.

import functools

def check(function):
    @functools.wraps(function)
    def wrapper(*args, **kwargs):
        if all(isinstance(x, type) for x in args + tuple(kwargs.values())):
            return function(*args, **kwargs)
        else:
            print(f'Tous les paramètres ne sont pas des {type}')

    return wrapper


@check
def add(a, b):
    """
    Cette fonction additionne deux entiers
    Args:
        a (int): Premier entier a additionner
        b (int): Second entier a additionner

    Returns:
        int : La somme entre a et b
    """
    return a + b


help(add)

# Help on function add in module __main__:
# 
# add(a, b)
#     Cette fonction additionne deux entiers
#     Args:
#         a (int): Premier entier a additionner
#         b (int): Second entier a additionner
#    
#     Returns:
#         int : La somme entre a et b

 

Enchaînement

La dernière chose importante est qu'il tout à fait possible d'enchaîner les décorateurs.

@checkargument(int)
@justrun
def add(a, b):
    return a + b


add(5, 3)

# None

Cela a pour effet de les jouer les un à la suite des autres. Ici @checkargument(int) va vérifier que les paramètres sont bien des int, puis va transmettre la fonction à @justrun qui va lancer la fonction add() sans retourner de résultat.

 

Conclusion

Cet article a permis d'éclairer le fonctionnement des décorateurs, et comment en construire. Les décorateurs sont une manière très discrète et très rapide pour jouer sur le comportement d'une fonction. Mais le fait de trop en utiliser peut rapidement rendre le code illisible, surtout pour les personnes novices voulant relire votre code (voir des fonctions de fonctions n'est pas très intuitif). Il existe aussi d'autres décorateurs natifs comme @cache permettant de garder les résultats d'une fonction mémoire, mais pour cela je vous laisserais aller voir sa documentation et les autres décorateurs proposés sur la documentation officielle de python.

le 26 nov. 2024 07:28

Ajouter un commentaire

Vous devez avoir un compte actif pour laisser des commentaires. Veuillez vous connecter, ou vous creer un compte.

0 Commentaire

Il n'y a pas encore de commentaire. Sois le premier à en déposer un !