les décorateurs en python

24 Apr 2019 24 Apr 2019 11035 vues ESSADDOUKI Mostafa 8 min de lecture

Les décorateurs

Les décorateurs sont des outils très puissants et utiles en Python, car ils permettent aux programmeurs de modifier le comportement d'une fonction ou d'une classe. Ils permettent d'envelopper (wrapper) une autre fonction afin d'étendre son comportement, sans la modifier de façon permanente.

Définition Un décorateur est une fonction qui prend une autre fonction en argument, lui ajoute un comportement supplémentaire, et retourne une nouvelle fonction. On l'applique avec la syntaxe @nom_decorateur placée juste avant la définition de la fonction cible.
Syntaxe générale
def decorateur(func):
    def wrapper(*args, **kwargs):
        # comportement avant
        resultat = func(*args, **kwargs)
        # comportement après
        return resultat
    return wrapper

@decorateur
def ma_fonction():
    ...
Principe Écrire @decorateur juste avant def ma_fonction(): est strictement équivalent à écrire : ma_fonction = decorateur(ma_fonction) après la définition. C'est uniquement une syntaxe plus lisible.

Premier exemple — Ajouter un message

L'exemple suivant crée un décorateur qui ajoute automatiquement « Bonjour » avant le résultat de n'importe quelle fonction renvoyant une chaîne.

Exemple

def decorateur(func):

    # Fonction interne qui ajoute "Bonjour" avant l'appel
    def wrapper(para):
        return "Bonjour " + func(para)

    return wrapper

@decorateur
def saluer(nom):
    return nom

print(saluer("Mostafa"))   # -> Bonjour Mostafa
Sortie
Bonjour Mostafa

Voici ce qui se passe étape par étape :

  1. saluer est passée en argument à decorateur.
  2. decorateur retourne wrapper, qui remplace saluer.
  3. À l'appel de saluer("Mostafa"), c'est wrapper("Mostafa") qui s'exécute.

Attacher des données à une fonction

Les décorateurs peuvent également être utiles pour attacher des données (ajouter un attribut) directement à une fonction. Cela permet de stocker des métadonnées associées à la fonction.

Exemple — Ajouter un attribut

def add_data(func):
    func.donnee = 5    # On attache un attribut directement à la fonction
    return func        # On retourne la fonction enrichie (pas de wrapper ici)

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

print(somme(3, 4))    # 7  — comportement normal
print(somme.donnee)   # 5  — attribut attaché par le décorateur
Sortie
7
5
  • Appeler somme(3, 4) renvoie simplement la somme 7.
  • Appeler somme.donnee retourne 5 : la fonction a été enrichie par add_data d'un attribut supplémentaire.
Cas d'usage Cette technique est utilisée en pratique pour attacher des métadonnées à des fonctions : numéro de version, permissions d'accès, description, URL dans un framework web, etc. Par exemple, Flask utilise @app.route("/chemin") qui attache l'URL à la fonction de vue.

Mesurer le temps d'exécution

Un cas d'usage classique des décorateurs est de mesurer automatiquement le temps d'exécution d'une fonction, sans modifier son code.

Les arguments *args et **kwargs Pour que le décorateur soit universel (applicable à toute fonction, quels que soient ses paramètres), la fonction interne wrapperaccepte :
  • *args : récupère tous les arguments positionnels sous forme de tuple.
  • **kwargs : récupère tous les arguments nommés sous forme de dictionnaire.

Exemple — Décorateur de chronométrage

import time

def temps_execution(func):
    def wrapper(*args, **kwargs):
        debut = time.time()             # horodatage avant
        resultat = func(*args, **kwargs)
        fin = time.time()               # horodatage après
        print(f"Temps d'exécution de '{func.__name__}' : {fin - debut:.6f} s")
        return resultat
    return wrapper

@temps_execution
def factorielle(num):
    f = 1
    for i in range(2, num + 1):
        f *= i
    print(f"factorielle({num}) = {f}")

factorielle(70)
Sortie
factorielle(70) = 119785716699698917960727837...
Temps d'exécution de 'factorielle' : 0.000021 s
Astuce — func.__name__ L'attribut __name__ d'une fonction contient son nom sous forme de chaîne. Il est très utile dans les décorateurs pour afficher ou journaliser le nom de la fonction décorée sans le coder en dur.

Décorateurs avec paramètres

Il est possible de passer des paramètres à un décorateur. Pour cela, on ajoute un niveau d'imbrication supplémentaire : une fonction externe reçoit les paramètres et retourne le décorateur réel.

Syntaxe
@nom_decorateur(params)
def nom_fonction():
    ...
Structure à 3 niveaux
  1. Niveau 1 : fonction externe qui reçoit les paramètres du décorateur.
  2. Niveau 2 : décorateur réel qui reçoit la fonction cible.
  3. Niveau 3 : fonction wrapper qui reçoit les arguments de la fonction.

Exemple — Décorateur avec paramètres nommés

def decorateur(*args, **kwargs):

    def appliquer(func):
        print(f"Année  : {args[0]}")
        print(f"Site   : {kwargs['site']}")
        print(f"Ville  : {kwargs['ville']}")
        return func

    return appliquer

@decorateur(2019, site="developpement-informatique.com", ville="Meknès")
def test():
    print("Fonction test exécutée")

test()
Sortie
Année  : 2019
Site   : developpement-informatique.com
Ville  : Meknès
Fonction test exécutée

Exemple — Décorateur de répétition

Un exemple plus pédagogique : un décorateur qui répète l'appel d'une fonction n fois.

def repeter(n):
    def decorateur(func):
        def wrapper(*args, **kwargs):
            for _ in range(n):
                func(*args, **kwargs)
        return wrapper
    return decorateur

@repeter(3)
def saluer(nom):
    print(f"Bonjour {nom} !")

saluer("Mostafa")
Sortie
Bonjour Mostafa !
Bonjour Mostafa !
Bonjour Mostafa !

Mémorisation à l'aide de décorateurs

Le problème avec la récursivité naïve

La récursivité est une technique dans laquelle une fonction s'appelle de manière répétée jusqu'à ce qu'une condition de terminaison soit remplie. Elle est utilisée par exemple pour calculer les suites de Fibonacci, les factorielles, etc.

Problème de performance Dans l'arbre de récursivité, il est possible que des sous-problèmes déjà résolus soient recalculés, ce qui entraîne une surcharge exponentielle. Par exemple, pour fib(5), fib(2) est recalculé 3 fois !

Voici l'arbre d'appels récursifs de fib(5) sans mémorisation :

Arbre d'appels — fib(5) sans mémorisation
fib(5)
├── fib(4)
│   ├── fib(3)
│   │   ├── fib(2) ← recalculé
│   │   └── fib(1)
│   └── fib(2)     ← recalculé
└── fib(3)
    ├── fib(2)     ← recalculé
    └── fib(1)

La mémorisation comme solution

La mémorisation (memoization) est une technique d'enregistrement des résultats intermédiaires afin d'éviter des calculs répétés et d'accélérer les programmes. En Python, elle peut être réalisée élégamment à l'aide d'un décorateur.

Définition — Mémorisation La mémorisation consiste à stocker le résultat d'un appel de fonction dans un cache (dictionnaire), et à retourner directement la valeur stockée si la même entrée est demandée à nouveau, sans recalculer.

Exemple — Suite de Fibonacci mémorisée

# Décorateur de mémorisation
def memo_fib(func):
    memoire = {}   # Cache : { num: résultat }

    def calculer(num):
        if num not in memoire:
            memoire[num] = func(num)   # Calcul et stockage
        return memoire[num]            # Retour depuis le cache

    return calculer

@memo_fib
def fib(num):
    if num < 2:
        return 1
    else:
        return fib(num - 1) + fib(num - 2)

print(fib(5))    # 8
print(fib(10))   # 89
print(fib(30))   # 1346269
Sortie
8
89
1346269

Voici comment fonctionne la mémorisation :

  • memo_fib : stocke les résultats intermédiaires dans le dictionnaire memoire.
  • fib : calcule la suite de Fibonacci. Grâce au concept de fermeture (closure), elle a accès à la variable memoire de la fonction englobante.
  • Lors de l'appel de fib(5), chaque valeur est calculée une seule fois puis mise en cache. Les appels suivants retournent instantanément la valeur stockée.

Solution intégrée : functools.lru_cache

Python fournit un décorateur de mémorisation intégré et optimisé dans le module functools : @lru_cache (Least Recently Used cache).

Exemple — Avec lru_cache

from functools import lru_cache

@lru_cache(maxsize=None)   # maxsize=None : cache illimité
def fib(n):
    if n < 2:
        return 1
    return fib(n - 1) + fib(n - 2)

print(fib(50))   # 20365011074
print(fib.cache_info())  # CacheInfo(hits=48, misses=51, ...)
Astuce En production, préférez @functools.lru_cache à un décorateur de mémorisation maison. Il est plus robuste, gère automatiquement la taille du cache, et fournit des statistiques via fonction.cache_info().

Récapitulatif des décorateurs vus

DécorateurRôleStructure
@decorateur simpleEnveloppe une fonction pour modifier son comportement2 niveaux : decorateurwrapper
@add_dataAttache un attribut à la fonction1 niveau, pas de wrapper
@temps_executionMesure le temps d'exécution2 niveaux avec *args, **kwargs
@decorateur(params)Décorateur paramétré3 niveaux : externe → décorateur → wrapper
@memo_fibMémorise les résultats (cache)2 niveaux + dictionnaire de cache
@lru_cacheMémorisation intégrée optimiséeModule functools (Python ≥ 3.2)

Discussion (0)

Soyez le premier à laisser un commentaire !

Laisser un commentaire

Votre commentaire sera visible après modération.