Comment utiliser les décorateurs ?

Introduction

Les décorateurs permettent de modifier les comportements des fonctions et/ou méthodes sans toucher au code source de ces dernières. Ils permettent de rendre le code plus clair et concis.
En utilisant le symbole @, ils permettent d'enrichir élégamment les fonctions et méthodes existantes.

Pour bien commencer

Les fonctions: des objets de première classe

En Python, comme évoqué dans le glossaire sur les fonctions, elles sont des objets de première classe.
C'est à dire que les fonctions peuvent être passées en argument, affectées à des variables et retournées par d'autres fonctions. Un concept important qui permet la création de décorateurs.

En effet, un décorateur est une fonction qui prend une autre fonction en argument et retourne une version modifiée de cette fonction.

Séparation et modularité

Dans un décorateur, il est intéressant de pouvoir regrouper des fonctionnalités communes, que l'on peut appliquer à différentes fonctions ou méthodes. Ce qui permet d'éviter de dupliquer le code.
De plus, les décorateurs permettent d'isoler le code principal dans la fonction, et les fonctionnalités transversales comme la journalisation et les permissions dans le décorateur.

Syntaxe et Mécanismes

Fonction décoratrice sans le symbole @

Sans utiliser la syntaxe avec le symbole @, un décorateur est une fonction qui accepte une fonction en paramètre et qui retourne une nouvelle fonction. On aura souvent le terme de wrapper.

def fonction_decoratrice(autre_fonction):
    def wrapper(*args, **kwargs):
        # Code exécuté avant l'appel de la fonction décorée
        print("Avant l'exécution de", autre_fonction.__name__)
        # Appel de la fonction décorée avec ses arguments
        result = autre_fonction(*args, **kwargs)
        # Code exécuté après l'appel de la fonction décorée
        print("Après l'exécution de", autre_fonction.__name__)
        return result
    return wrapper


def saluer(nom):
    print(f"Bonjour, {nom}!")

# Application manuelle du décorateur
saluer = fonction_decoratrice(saluer)

# Maintenant, en appelant saluer, on déclenche le code du wrapper
saluer("Alice")
Un instant

  • fonction_decoratrice est notre décorateur. Il prend en argument autre_fonction, la fonction à décorer.

  • wrapper est la fonction interne qui reçoit les arguments positionnels et nommés via *args et **kwargs.

Nous sommes d'accord pour dire que cette manière de faire paraît un peu compliquée...

Fonction décoratrice avec le symbole @

Python permet d'utiliser les décorateurs de manière plus élégante (et plus simple !). Il est donc possible de remplacer :

def saluer(nom):
    print(f"Bonjour, {nom}!")

# Application manuelle du décorateur
saluer = fonction_decoratrice(saluer)

par cette syntaxe plus épurée :

@fonction_decoratrice
def saluer(nom):
    print(f"Bonjour, {nom}!")

Conserver les métadonnées avec functools.wraps

A noter que lorsque vous créez un décorateur, un problème subtil apparaît : la fonction décorée perd ses données originales, telles que son nom et sa documentation. En réalité, la fonction originale va être remplacée par le wrapper.

Heureusement, il existe un moyen pour maintenir l'intégrité de la fonction décorée avec le module functools.

Exemple sans functools.wraps :

def fonction_decoratrice(autre_fonction):
    def wrapper(*args, **kwargs):
        print("Avant l'exécution de", autre_fonction.__name__)
        result = autre_fonction(*args, **kwargs)
        print("Après l'exécution de", autre_fonction.__name__)
        return result
    return wrapper

@fonction_decoratrice
def saluer(nom):
    """Affiche un message de salutation."""
    print(f"Bonjour, {nom}!")

print(saluer.__name__)  # Affiche 'wrapper'
print(saluer.__doc__)   # Affiche None
Un instant

Puis avec functools.wraps :

from functools import wraps

def fonction_decoratrice(autre_fonction):
    @wraps(autre_fonction)
    def wrapper(*args, **kwargs):
        print("Avant l'exécution de", autre_fonction.__name__)
        result = autre_fonction(*args, **kwargs)
        print("Après l'exécution de", autre_fonction.__name__)
        return result
    return wrapper

@fonction_decoratrice
def saluer(nom):
    """Affiche un message de salutation."""
    print(f"Bonjour, {nom}!")

print(saluer.__name__)  # Affiche 'saluer'
print(saluer.__doc__)   # Affiche 'Affiche un message de salutation.'
Un instant

Les décorateurs avec paramètres

Les décorateurs paramétrés acceptent des arguments qui permettent de configurer leur propre comportement.

import functools

def mon_decorateur(param):
    def decorateur(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            print(f"Avant l'exécution, paramètre: {param}")
            result = func(*args, **kwargs)
            print(f"Après l'exécution, paramètre: {param}")
            return result
        return wrapper
    return decorateur

@mon_decorateur("Test")
def ma_fonction():
    print("Exécution de ma_fonction")

ma_fonction()
Un instant

La structure :

  • Une fonction externe,

  • Le décorateur (fonction intermédiaire),

  • Le wrapper (fonction interne)

Le décorateur paramétré est donc une fonction qui retourne un décorateur.

Un exemple parlant avec Django :

from django.contrib.auth.decorators import user_passes_test

def is_admin(user):
    """Vérifie si l'utilisateur est administrateur en testant l'attribut is_staff."""
    return user.is_staff

@user_passes_test(is_admin)
def my_view(request):
    """
    Vue accessible uniquement aux utilisateurs pour lesquels is_admin retourne True.
    Si l'utilisateur ne satisfait pas la condition, il sera redirigé vers la page de connexion par défaut.
    """
    # Traitement de la vue
    ...

Dans ce cas, le décorateur permet une logique supplémentaire de permission et de login par rapport à la fonction.

Décorer une méthode

Décorer une méthode permet de modifier ou étendre son comportement, tout comme avec les fonctions.

def log_decorator(methode):
    def wrapper(*args, **kwargs):
        print(f"Appel de la méthode {methode.__name__}")
        return methode(*args, **kwargs)
    return wrapper

class Calculatrice:
    @log_decorator
    def additionner(self, a, b):
        return a + b


c = Calculatrice()
c.additionner(4, 4)
Un instant

Décorer une classe

Décorer une classe permet de modifier ou étendre la classe entière.
De cette manière on peut :

  • Modifier les méthodes existantes

  • Ajouter des méthodes et des attributs

# Décorateur de classe
def ameliorer_classe(cls):
    # Modification d'une méthode existante
    methode_originale = cls.afficher_info

    def nouvelle_afficher_info(self):
        print("Version améliorée:")
        methode_originale(self)
        print(f"Prix avec taxe: {self.prix * 1.2:.2f}€")

    cls.afficher_info = nouvelle_afficher_info

    # Ajout d'une nouvelle méthode
    def appliquer_remise(self, pourcentage):
        self.prix = self.prix * (1 - pourcentage / 100)
        print(f"Remise de {pourcentage}% appliquée! Nouveau prix: {self.prix:.2f}€")

    cls.appliquer_remise = appliquer_remise

    # Ajout d'un nouvel attribut
    cls.origine = "France"

    return cls

# Utilisation du décorateur
@ameliorer_classe
class Produit:
    def __init__(self, nom, prix):
        self.nom = nom
        self.prix = prix

    def afficher_info(self):
        print(f"Produit: {self.nom}")
        print(f"Prix: {self.prix:.2f}€")


p = Produit("Livre Python", 29.99)
p.afficher_info()
print(f"Origine: {p.origine}")
p.appliquer_remise(15)
p.afficher_info()
Un instant

Empiler les décorateurs

Il est possible d'appliquer plusieurs décorateurs à une fonction :

def decorateur1(fonction):
    def wrapper(*args, **kwargs):
        print("Décorateur 1 - Avant l'exécution")
        resultat = fonction(*args, **kwargs)
        print("Décorateur 1 - Après l'exécution")
        return resultat
    return wrapper

def decorateur2(fonction):
    def wrapper(*args, **kwargs):
        print("Décorateur 2 - Avant l'exécution")
        resultat = fonction(*args, **kwargs)
        print("Décorateur 2 - Après l'exécution")
        return resultat
    return wrapper



@decorateur1
@decorateur2
def ma_fonction(texte):
    print(f"Exécution de la fonction avec: {texte}")
    return texte.upper()

# Test de la fonction décorée
resultat = ma_fonction("Bonjour le monde")
print(f"Résultat: {resultat}")
Un instant

En Python, l'empilement des décorateurs se fait selon cet ordre : ils sont traités du bas vers le haut.

Bravo, tu es prêt à passer à la suite

Rechercher sur le site

Formulaire de contact

Inscris-toi à Docstring

Pour commencer ton apprentissage.

Tu as déjà un compte ? Connecte-toi.