> Guide complet du décorateur Python : fonctions comme objets, closures, syntaxe @, décorateurs avec arguments, functools.wraps et cas d'usage (log, timing, cache).

*Source : https://coder-studio.com/tutoriels/decorateurs-python/*

---

[// Python](https://coder-studio.com/tutoriels/)

# Les décorateurs en Python : comprendre et les utiliser

Par **Kévin Papot** · publié le 30 octobre 2025

Un **décorateur Python** est une fonction qui prend une autre fonction en entrée, lui ajoute un comportement, puis renvoie une nouvelle fonction. C’est l’un des outils les plus élégants du langage : il permet d’enrichir du code (journalisation, chronométrage, mise en cache, contrôle d’accès) sans toucher à la fonction d’origine. Ce guide couvre le sujet de façon exhaustive : les fonctions comme objets, les closures, la syntaxe `@`, les décorateurs avec et sans arguments, `functools.wraps`, les cas d’usage concrets et les décorateurs de classe.

## Les fonctions sont des objets de première classe

Avant de comprendre les décorateurs, il faut saisir un principe fondamental : en Python, **une fonction est un objet comme un autre**. On peut la stocker dans une variable, la passer en argument ou la renvoyer.

```
def crier(texte):
    return texte.upper() + " !"

# On assigne la fonction à une variable (sans les parenthèses)
f = crier
print(f("bonjour"))   # BONJOUR !

# On la passe en argument
def appliquer(fonction, valeur):
    return fonction(valeur)

print(appliquer(crier, "salut"))   # SALUT !
```

Notez la différence : `crier` désigne l’objet fonction, tandis que `crier("bonjour")` l’**appelle**. Cette distinction est la clé de tout ce qui suit.

## Les closures : des fonctions qui se souviennent

Une **closure** est une fonction interne qui capture les variables de la fonction qui l’entoure, même après que celle-ci a terminé son exécution.

```
def multiplicateur(facteur):
    def multiplier(nombre):
        return nombre * facteur   # 'facteur' est capturé
    return multiplier

double = multiplicateur(2)
triple = multiplicateur(3)

print(double(10))   # 20
print(triple(10))   # 30
```

`multiplier` se « souvient » de la valeur de `facteur` qui lui a été fournie. Un décorateur n’est rien d’autre qu’une closure appliquée à une fonction.

## Écrire son premier décorateur

Le schéma canonique : une fonction externe reçoit `fonction`, définit une fonction interne `wrapper` qui ajoute du comportement, puis renvoie `wrapper`.

```
def journaliser(fonction):
    def wrapper():
        print(f"Appel de {fonction.__name__}")
        resultat = fonction()
        print("Terminé")
        return resultat
    return wrapper

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

dire_bonjour = journaliser(dire_bonjour)
dire_bonjour()
```

Sortie :

```
Appel de dire_bonjour
Bonjour !
Terminé
```

La ligne `dire_bonjour = journaliser(dire_bonjour)` remplace la fonction d’origine par sa version décorée.

## La syntaxe @ : le sucre syntaxique

Écrire `ma_fonction = decorateur(ma_fonction)` est répétitif. Python fournit l’opérateur `@`, placé juste au-dessus de la définition, qui fait exactement la même chose.

```
def journaliser(fonction):
    def wrapper():
        print(f"Appel de {fonction.__name__}")
        return fonction()
    return wrapper

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

dire_bonjour()
```

`@journaliser` au-dessus de `dire_bonjour` équivaut strictement à `dire_bonjour = journaliser(dire_bonjour)`. C’est purement une commodité de lecture.

## Décorer des fonctions avec arguments

Le `wrapper` de l’exemple précédent ne prend aucun argument. Pour décorer n’importe quelle fonction, on utilise `*args` et `**kwargs`, qui capturent tous les arguments positionnels et nommés. Si le sujet vous échappe, notre guide sur [les fonctions en Python](https://coder-studio.com/tutoriels/fonctions-python/) détaille ce mécanisme.

```
def journaliser(fonction):
    def wrapper(*args, **kwargs):
        print(f"Appel de {fonction.__name__} avec {args} {kwargs}")
        resultat = fonction(*args, **kwargs)
        print(f"Retour : {resultat}")
        return resultat
    return wrapper

@journaliser
def additionner(a, b):
    return a + b

additionner(3, 5)
```

Sortie :

```
Appel de additionner avec (3, 5) {}
Retour : 8
```

Le `wrapper` transmet fidèlement les arguments à la fonction d’origine, quelle que soit sa signature.

## Préserver les métadonnées avec functools.wraps

Un problème subtil apparaît : après décoration, la fonction perd son nom et sa docstring, remplacés par ceux du `wrapper`.

```
@journaliser
def additionner(a, b):
    """Additionne deux nombres."""
    return a + b

print(additionner.__name__)   # wrapper  (au lieu de additionner !)
print(additionner.__doc__)    # None
```

La solution est le décorateur `functools.wraps`, appliqué au `wrapper`. Il copie les métadonnées de la fonction d’origine.

```
import functools

def journaliser(fonction):
    @functools.wraps(fonction)
    def wrapper(*args, **kwargs):
        return fonction(*args, **kwargs)
    return wrapper

@journaliser
def additionner(a, b):
    """Additionne deux nombres."""
    return a + b

print(additionner.__name__)   # additionner
print(additionner.__doc__)    # Additionne deux nombres.
```

**Utilisez toujours `functools.wraps`** dans vos décorateurs : c’est une bonne pratique indispensable pour le débogage et l’introspection.

## Les décorateurs avec arguments

Parfois on veut paramétrer le décorateur lui-même, par exemple `@repeter(3)`. Cela demande un **niveau d’imbrication supplémentaire** : une fonction qui reçoit les arguments et renvoie un décorateur.

```
import functools

def repeter(nombre):
    def decorateur(fonction):
        @functools.wraps(fonction)
        def wrapper(*args, **kwargs):
            for _ in range(nombre):
                resultat = fonction(*args, **kwargs)
            return resultat
        return wrapper
    return decorateur

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

saluer("Léa")
```

Sortie :

```
Bonjour Léa
Bonjour Léa
Bonjour Léa
```

La logique se lit de l’extérieur vers l’intérieur : `repeter(3)` renvoie `decorateur`, qui décore `saluer`. Trois couches de fonctions, mais un motif toujours identique.

## Cas d’usage n°1 : chronométrer une fonction (timing)

Mesurer le temps d’exécution est le cas d’école du décorateur.

```
import functools
import time

def chronometrer(fonction):
    @functools.wraps(fonction)
    def wrapper(*args, **kwargs):
        debut = time.perf_counter()
        resultat = fonction(*args, **kwargs)
        duree = time.perf_counter() - debut
        print(f"{fonction.__name__} a pris {duree:.4f} s")
        return resultat
    return wrapper

@chronometrer
def calcul_lourd(n):
    return sum(i * i for i in range(n))

calcul_lourd(1_000_000)
```

## Cas d’usage n°2 : journaliser les appels (log)

Tracer les entrées et sorties d’une fonction aide au débogage sans polluer sa logique.

```
import functools

def tracer(fonction):
    @functools.wraps(fonction)
    def wrapper(*args, **kwargs):
        signature = ", ".join([repr(a) for a in args] +
                              [f"{k}={v!r}" for k, v in kwargs.items()])
        print(f"→ {fonction.__name__}({signature})")
        resultat = fonction(*args, **kwargs)
        print(f"← {resultat!r}")
        return resultat
    return wrapper

@tracer
def diviser(a, b):
    return a / b

diviser(10, 4)
```

## Cas d’usage n°3 : mise en cache (cache)

Éviter de recalculer un résultat déjà connu. Python fournit directement `functools.lru_cache`, un décorateur prêt à l’emploi.

```
import functools

@functools.lru_cache(maxsize=None)
def fibonacci(n):
    if n < 2:
        return n
    return fibonacci(n - 1) + fibonacci(n - 2)

print(fibonacci(50))   # instantané grâce au cache
```

Sans le cache, `fibonacci(50)` déclencherait des milliards d’appels. Avec `lru_cache`, chaque valeur n’est calculée qu’une fois.

## Empiler plusieurs décorateurs

On peut appliquer plusieurs décorateurs à une même fonction. Ils s’appliquent **de bas en haut**.

```
@tracer
@chronometrer
def travailler(n):
    return sum(range(n))

travailler(100)
```

Ici `travailler` est d’abord enveloppée par `chronometrer`, puis le résultat par `tracer`. L’ordre a son importance.

## Les décorateurs de classe

Un décorateur peut aussi cibler une **classe** entière, pour lui ajouter des attributs ou modifier son comportement.

```
def ajouter_identite(cls):
    cls.categorie = "outil"
    return cls

@ajouter_identite
class Marteau:
    pass

print(Marteau.categorie)   # outil
```

À l’inverse, on peut utiliser une **classe comme décorateur** en implémentant la méthode `__call__`, ce qui permet de stocker un état entre les appels.

```
import functools

class Compteur:
    def __init__(self, fonction):
        functools.update_wrapper(self, fonction)
        self.fonction = fonction
        self.appels = 0

    def __call__(self, *args, **kwargs):
        self.appels += 1
        print(f"Appel n°{self.appels}")
        return self.fonction(*args, **kwargs)

@Compteur
def bonjour():
    print("Salut")

bonjour()   # Appel n°1
bonjour()   # Appel n°2
```

## Conclusion

Les décorateurs reposent sur trois idées simples : les fonctions sont des objets, les closures capturent leur contexte, et la syntaxe `@` n’est qu’un raccourci pour `f = decorateur(f)`. Maîtrisez le motif `wrapper(*args, **kwargs)` avec `functools.wraps`, et vous pourrez ajouter proprement du logging, du timing ou du cache à n’importe quelle fonction. C’est un outil incontournable de tout code Python professionnel.

## À lire ensuite

-   [Les fonctions en Python : tout ce qu’il faut savoir](https://coder-studio.com/tutoriels/fonctions-python/)
-   [Les classes en Python et la POO](https://coder-studio.com/tutoriels/classes-python-poo/)
-   [Apprendre Python : par où commencer](https://coder-studio.com/tutoriels/apprendre-python/)
