# Décorateurs en Python : ce que c'est, comment ils fonctionnent et comment en écrire un

URL: https://skillaria.top/fr/guides/python/decorateurs-python

Un décorateur en Python est une fonction qui reçoit une autre fonction et renvoie une nouvelle fonction, en général un wrapper qui ajoute un comportement avant ou après l'appel : journalisation, mesure du temps, cache, contrôle des droits. La syntaxe @decorator placée au-dessus du def équivaut à func = decorator(func) ; pour que le wrapper garde le nom et la docstring de la fonction d'origine, on utilise functools.wraps.

Un décorateur en Python est une fonction qui reçoit une fonction et renvoie une autre fonction, généralement un wrapper qui entoure la première. C'est ainsi qu'on ajoute un comportement à une fonction — journalisation, mesure du temps, cache, nouvelles tentatives — sans modifier son code. Les décorateurs s'écrivent avec la syntaxe `@nom` au-dessus de la déclaration de la fonction, et on les rencontre partout : dans Flask et FastAPI, dans pytest, dans la bibliothèque standard.

## Ce qu'est un décorateur, en termes simples

Pensez à un cadeau et à son papier d'emballage. Le cadeau (la fonction) reste le même, mais une couche extérieure entre en action en premier. Un décorateur est exactement cet emballage : à l'appel, le code du wrapper s'exécute d'abord, appelle la fonction d'origine, et peut ensuite traiter le résultat.

Cela fonctionne parce qu'en Python les fonctions sont des objets ordinaires. On peut les passer à d'autres fonctions, les renvoyer depuis une fonction, et les déclarer à l'intérieur d'une fonction. Les décorateurs reposent sur ces trois propriétés.

## Comment fonctionne un décorateur : @ est un raccourci de syntaxe

Prenons le décorateur le plus simple : il affiche un message avant et après l'appel.

```
def journaliser(func):
    def wrapper():
        print(f"J'appelle {func.__name__}")
        resultat = func()
        print(f"{func.__name__} est terminée")
        return resultat
    return wrapper

@journaliser
def bonjour():
    print("Salut !")

bonjour()
```

```
J'appelle bonjour
Salut !
bonjour est terminée
```

L'écriture `@journaliser` placée au-dessus de `def` est strictement équivalente à la ligne suivante, écrite après la déclaration :

```
def bonjour():
    print("Salut !")

bonjour = journaliser(bonjour)
```

Le nom `bonjour` désigne désormais `wrapper`, et la fonction d'origine vit à l'intérieur de la fermeture : `wrapper` conserve la variable `func` de la fonction externe. Point important : le décorateur n'est appliqué qu'une fois, au moment où `def` s'exécute, c'est-à-dire à l'import du module ; le wrapper, lui, se déclenche à chaque appel.

## Fermetures et décorateurs

Une fermeture (closure) est une fonction imbriquée qui mémorise les variables de la fonction externe après la fin de celle-ci. Sans fermetures, il n'y aurait pas de décorateurs : c'est grâce à elles que `wrapper` accède à `func`. Une fermeture peut aussi conserver un état, par exemple un compteur d'appels :

```
def compter_appels(func):
    appels = 0
    def wrapper(*args, **kwargs):
        nonlocal appels
        appels += 1
        print(f"{func.__name__} : appel n°{appels}")
        return func(*args, **kwargs)
    return wrapper
```

## Écrire un décorateur en Python : modèle universel

Le premier exemple a deux défauts : le wrapper n'accepte pas d'arguments, et il « avale » le nom de la fonction d'origine. Voici le modèle de départ, valable pour presque tous les décorateurs :

```
import functools

def mon_decorateur(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        # code avant l'appel
        resultat = func(*args, **kwargs)
        # code après l'appel
        return resultat
    return wrapper
```

Trois règles pour ce modèle :

1. **`*args, **kwargs`** — le wrapper accepte n'importe quels arguments et les transmet tels quels.
2. **`return resultat`** — ne pas oublier de renvoyer le résultat, sinon la fonction décorée renverra toujours `None`.
3. **`@functools.wraps(func)`** — recopier les métadonnées de la fonction d'origine sur le wrapper.

Exemple de décorateur qui mesure le temps d'exécution :

```
import functools
import time

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

@chronometre
def somme_lente(n):
    return sum(range(n))

somme_lente(10_000_000)   # somme_lente a pris 0.142 s
```

## À quoi sert functools.wraps

Sans `wraps`, le nom, la docstring et d'autres attributs de la fonction décorée sont remplacés : de l'extérieur, on voit `wrapper` :

```
def simple(func):
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

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

print(additionner.__name__, additionner.__doc__)   # wrapper None
```

Avec `@functools.wraps(func)`, le wrapper reçoit `__name__`, `__doc__`, `__module__`, `__qualname__` et `__annotations__` de la fonction d'origine, et l'attribut `__wrapped__` conserve une référence vers l'original. C'est important pour le débogage et les traces d'erreur, pour `help()`, pour les frameworks qui enregistrent les fonctions par leur nom, et pour `inspect.signature`, qui affiche la vraie signature.

## Décorateur avec paramètres (avec arguments)

Parfois, un décorateur a besoin de réglages : `@retry(times=3)`, `@app.route("/")`. On ajoute alors un niveau d'imbrication. La fonction externe reçoit les paramètres et renvoie le véritable décorateur : c'est ce qu'on appelle une fabrique de décorateurs.

```
import functools

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

@repeter(fois=3)
def ping():
    print("ping")

ping()   # ping, ping, ping — trois lignes
```

Ordre d'évaluation : `repeter(fois=3)` renvoie `decorateur`, qui est appliqué à `ping` et renvoie `wrapper`. Autrement dit, `@repeter(fois=3)` revient à écrire `ping = repeter(fois=3)(ping)`.

Exemple pratique : relancer l'appel en cas d'erreur.

```
import functools
import time

def reessayer(fois=3, delai=1.0, exceptions=(Exception,)):
    def decorateur(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            for tentative in range(1, fois + 1):
                try:
                    return func(*args, **kwargs)
                except exceptions:
                    if tentative == fois:
                        raise
                    time.sleep(delai)
        return wrapper
    return decorateur

@reessayer(fois=5, delai=0.5, exceptions=(ConnectionError,))
def obtenir_taux():
    ...
```

## Plusieurs décorateurs sur une même fonction

On peut empiler les décorateurs. Ils s'appliquent de bas en haut : celui qui est le plus proche du `def` agit en premier.

```
@chronometre
@journaliser
def rapport():
    ...

# équivalent à :
rapport = chronometre(journaliser(rapport))
```

À l'appel, le décorateur externe (`chronometre`) se déclenche en premier, il mesure donc aussi le temps passé dans `journaliser`. L'ordre compte : si l'un vérifie les droits d'accès et l'autre met le résultat en cache, leur ordre change le comportement.

## Décorateurs standard et intégrés de Python

Beaucoup de décorateurs utiles existent déjà dans le langage et dans la bibliothèque standard :

| Décorateur | Ce qu'il fait |
| --- | --- |

| `@property` | transforme une méthode en attribut en lecture seule ; avec `@x.setter`, en attribut contrôlé |

| `@staticmethod` | méthode de classe sans `self`, simple fonction rangée dans l'espace de noms de la classe |

| `@classmethod` | la méthode reçoit la classe `cls` au lieu de l'instance — c'est ainsi qu'on écrit des constructeurs alternatifs |

| `@functools.cache`, `@functools.lru_cache` | mettent en cache les résultats selon les arguments |

| `@functools.wraps` | recopie les métadonnées de la fonction sur le wrapper |

| `@dataclasses.dataclass` | génère `__init__`, `__repr__`, `__eq__` pour une classe |

| `@contextlib.contextmanager` | transforme un générateur en gestionnaire de contexte pour `with` |

### Mise en cache : functools.cache et lru_cache

Un décorateur de cache mémorise le résultat pour chaque ensemble d'arguments et, à l'appel suivant, le renvoie sans recalcul. L'exemple classique est la suite de Fibonacci récursive, qui, sans cache, demande un temps exponentiel :

```
import functools

@functools.cache
def fib(n):
    return n if n < 2 else fib(n - 1) + fib(n - 2)

print(fib(100))   # 354224848179261915075
```

`functools.cache` (disponible depuis Python 3.9) conserve tous les résultats sans limite. `@functools.lru_cache(maxsize=128)` ne garde que les derniers résultats et évince les plus anciens : c'est plus sûr pour les services qui tournent longtemps. On consulte les statistiques avec `fib.cache_info()` et on vide le cache avec `fib.cache_clear()`. Les arguments de ces fonctions doivent être hachables : une liste passée en argument provoque `TypeError: unhashable type: 'list'`. Et il ne faut mettre en cache que des fonctions pures, c'est-à-dire qui renvoient toujours la même chose pour les mêmes arguments et n'ont pas d'effets de bord.

### Ce que fait le décorateur property

`@property` permet d'accéder à une méthode comme à un attribut, sans parenthèses, et de contrôler l'affectation si nécessaire :

```
class Compte:
    def __init__(self, solde):
        self._solde = solde

    @property
    def solde(self):
        return self._solde

    @solde.setter
    def solde(self, valeur):
        if valeur < 0:
            raise ValueError("Le solde ne peut pas être négatif")
        self._solde = valeur

compte = Compte(100)
print(compte.solde)   # 100
compte.solde = -5     # ValueError
```

### staticmethod et classmethod

```
class Montant:
    def __init__(self, centimes):
        self.centimes = centimes

    @classmethod
    def depuis_euros(cls, euros):
        return cls(round(euros * 100))

    @staticmethod
    def est_valide(valeur):
        return valeur >= 0

m = Montant.depuis_euros(12.5)
print(m.centimes, Montant.est_valide(-1))   # 1250 False
```

C'est `@classmethod` qui permet à une méthode de travailler avec la classe elle-même et non avec ses instances : `depuis_euros` crée correctement un objet de sous-classe.

## Décorateurs de classes et classes utilisées comme décorateurs

Un décorateur peut s'appliquer à une classe : il reçoit la classe et renvoie une classe, identique ou modifiée. C'est ainsi que fonctionne `@dataclass`. Exemple simple : un registre de classes.

```
registre = {}

def enregistrer(cls):
    registre[cls.__name__] = cls
    return cls

@enregistrer
class ExportateurCsv:
    pass

print(registre)   # {'ExportateurCsv': }
```

Le cas inverse, où une classe joue le rôle de décorateur, est aussi possible. Il suffit d'un objet doté de la méthode `__call__` ; l'état est commodément stocké dans les attributs de l'instance :

```
import functools

class CompteurAppels:
    def __init__(self, func):
        functools.update_wrapper(self, func)
        self.func = func
        self.appels = 0

    def __call__(self, *args, **kwargs):
        self.appels += 1
        return self.func(*args, **kwargs)

@CompteurAppels
def tic():
    pass

tic(); tic()
print(tic.appels)   # 2
```

## Décorateur asynchrone

Pour les fonctions `async def`, le wrapper doit lui aussi être une coroutine et attendre le résultat avec `await`, sinon on récupère un objet coroutine non exécuté :

```
import functools

def journaliser_async(func):
    @functools.wraps(func)
    async def wrapper(*args, **kwargs):
        print(f"Début de {func.__name__}")
        resultat = await func(*args, **kwargs)
        print(f"Fin de {func.__name__}")
        return resultat
    return wrapper
```

## À quoi servent les décorateurs en Python

- **Logique transversale** — journalisation, métriques, mesure du temps sans copier le code dans chaque fonction.
- **Mise en cache** — `@functools.cache` pour les calculs coûteux et purs.
- **Contrôles** — droits d'accès, validation des arguments, autorisation dans les gestionnaires web.
- **Enregistrement** — routes dans Flask et FastAPI (`@app.get("/")`), fixtures pytest.
- **Fiabilité** — nouvelles tentatives, délais d'attente, gestion des erreurs.

Le principal avantage est la séparation des responsabilités : la fonction s'occupe de sa tâche, et le comportement annexe s'ajoute en une ligne, tout aussi facile à retirer.

## Erreurs typiques

- **Le wrapper ne renvoie pas le résultat** — après décoration, la fonction renvoie `None`.
- **Pas de `*args, **kwargs`** — `TypeError` lors d'un appel avec des arguments.
- **Oubli de `wraps`** — `__name__` et docstring erronés, confusion dans les journaux.
- **Parenthèses oubliées sur une fabrique** — `@repeter` au lieu de `@repeter(3)` : la fonction est passée au paramètre `fois`.
- **Code lourd dans le corps du décorateur** — il s'exécute à l'import du module, et non à l'appel.

## En bref

Les décorateurs en Python sont des fonctions d'ordre supérieur : elles reçoivent une fonction, renvoient un wrapper, et le `@` n'est qu'une écriture courte d'affectation. Commencez par le modèle avec `*args, **kwargs`, `return resultat` et `functools.wraps`, puis ajoutez un niveau de fabrique pour les réglages. Les fonctions, les fermetures et les annotations sur lesquelles reposent les décorateurs sont mises en pratique dans le cours sur Python.

## FAQ

**Peut-on retirer un décorateur d'une fonction ?**

Si le décorateur utilise functools.wraps, l'original reste accessible par l'attribut __wrapped__ : func.__wrapped__(...) appelle la fonction sans le wrapper. Sinon, il faut supprimer la ligne avec @ dans le code.

**Quand s'exécute le code du décorateur ?**

Le corps du décorateur lui-même s'exécute une seule fois, au moment de la déclaration de la fonction, en général lors de l'import du module. Le code situé dans wrapper s'exécute à chaque appel de la fonction décorée.

**Peut-on décorer des méthodes de classe ?**

Oui, un décorateur ordinaire avec *args, **kwargs fonctionne aussi pour les méthodes : self se retrouve simplement en premier élément de args.

**Quelle est la différence entre un décorateur et l'héritage ?**

L'héritage modifie le comportement d'une classe par une hiérarchie, alors qu'un décorateur enveloppe de l'extérieur une fonction ou une classe précise, et se combine facilement avec d'autres décorateurs sans toucher au code.


---
Source: Skillaria.Top (https://skillaria.top)
