# Les décorateurs : ajouter un comportement à une fonction sans y toucher<no value>

## Qu'allons-nous apprendre ?

- Ce qu'est un **décorateur** (Decorator) et pourquoi il est utile.
- Comment construire un décorateur : une fonction qui reçoit une fonction et renvoie un **wrapper** (une enveloppe).
- La syntaxe **`@`**.
- Un wrapper adapté à **n'importe quelle** fonction, avec `*args` et `**kwargs`.

## Pourquoi en avons-nous besoin ?

Nous voulons afficher un message à chaque appel d'une fonction — pour le débogage :

```python
def add(a, b):
    print("Calling add")
    return a + b

def multiply(a, b):
    print("Calling multiply")
    return a * b
```

Les problèmes :

- **La même ligne est copiée** dans chaque fonction — encore la duplication que DRY interdit.
- **La logique se mélange** à du code d'aide qui n'a rien à voir avec elle.
- Pour **supprimer** les affichages, il faut parcourir toutes les fonctions et les effacer.

Un **décorateur** permet d'écrire le comportement supplémentaire **une seule fois**, et de le poser sur n'importe quelle fonction en une ligne.

## Explication à partir des bases

### De quoi est fait un décorateur

Un décorateur combine deux choses apprises dans les leçons précédentes :

- **La fonction d'ordre supérieur** — il reçoit une fonction et renvoie une fonction.
- **La closure** — la fonction qu'il renvoie se souvient de la fonction d'origine.

La structure est toujours la même :

1. Le décorateur reçoit la fonction d'origine (`func`).
2. À l'intérieur, on définit une nouvelle fonction — le **`wrapper`** — qui fait quelque chose de plus, **et appelle `func`**.
3. Le décorateur renvoie `wrapper` — **sans parenthèses**.

### La syntaxe @

```python
@log_call
def add(a, b):
    return a + b
```

C'est exactement comme écrire après la définition :

```python
add = log_call(add)
```

Autrement dit : le nom `add` pointe maintenant vers le **wrapper**, et le wrapper appelle l'`add` d'origine.

{{< box "analogy" "Analogie : emballer un cadeau" >}}
Le cadeau (la fonction) ne change pas. Vous l'enveloppez de papier (le wrapper) — et celui qui le reçoit rencontre d'abord l'emballage, et seulement ensuite le cadeau. On peut envelopper n'importe quel cadeau dans le même type de papier, et on peut retirer l'emballage sans abîmer le cadeau.
{{< /box >}}

## Syntaxe

```python
def my_decorator(func):              # 1. receive the original function
    def wrapper(*args, **kwargs):    # 2. a new function that wraps it
        # ... something before ...
        result = func(*args, **kwargs)
        # ... something after ...
        return result                # return the original result
    return wrapper                   # 3. return the wrapper (no parentheses)

@my_decorator                        # same as: my_function = my_decorator(my_function)
def my_function():
    ...
```

## Premier exemple de code

`week-2/log_call.py` :

```python
def log_call(func):
    def wrapper(a, b):
        print("Calling", func.__name__)
        return func(a, b)
    return wrapper

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

@log_call
def multiply(a, b):
    return a * b

print(add(2, 3))
print(multiply(4, 5))
```

**Sortie :**

```text
Calling add
5
Calling multiply
20
```

## Analyse du code ligne par ligne

| Ligne | Explication |
|---|---|
| `def log_call(func):` | Le décorateur reçoit la fonction d'origine. |
| `def wrapper(a, b):` | L'enveloppe — elle reçoit les mêmes paramètres que l'originale. |
| `print("Calling", func.__name__)` | Le comportement supplémentaire. `__name__` est le nom de la fonction. |
| `return func(a, b)` | Appelle l'originale, et **renvoie** son résultat. |
| `return wrapper` | Le décorateur renvoie le wrapper — sans parenthèses. |
| `@log_call` | `add = log_call(add)`. Désormais `add(2, 3)` exécute le wrapper. |

## Un autre exemple

**Un wrapper adapté à n'importe quelle fonction.** Le wrapper précédent ne fonctionne qu'avec des fonctions qui reçoivent exactement `a, b`. Avec `*args` et `**kwargs`, il reçoit **n'importe quel** paramètre et le transmet. `week-2/log_any.py` :

```python
def log_call(func):
    def wrapper(*args, **kwargs):
        print("Calling", func.__name__, "with", args, kwargs)
        return func(*args, **kwargs)
    return wrapper

@log_call
def greet(name, greeting="Hello"):
    return greeting + ", " + name

@log_call
def total(*prices):
    return sum(prices)

print(greet("Dana", greeting="Hi"))
print(total(10, 20, 30))
```

**Sortie :**

```text
Calling greet with ('Dana',) {'greeting': 'Hi'}
Hi, Dana
Calling total with (10, 20, 30) {}
60
```

**Un exemple utile : mesurer le temps.** `week-2/timed.py` :

```python
import time

def timed(func):
    def wrapper(*args, **kwargs):
        start = time.time()
        result = func(*args, **kwargs)
        seconds = round(time.time() - start, 2)
        print(func.__name__, "took", seconds, "seconds")
        return result
    return wrapper

@timed
def slow_sum(n):
    time.sleep(0.5)
    return sum(range(n))

print(slow_sum(1000))
```

**Sortie :**

```text
slow_sum took 0.5 seconds
499500
```

(Le temps exact varie un peu d'une exécution à l'autre.) Le même `@timed` peut être posé sur n'importe quelle fonction du programme — sans changer une seule ligne à l'intérieur.

## Erreur courante

**Deux erreurs qui arrivent à presque tous ceux qui écrivent un premier décorateur :**

```python
def log_call(func):
    def wrapper(*args, **kwargs):
        print("Calling", func.__name__)
        func(*args, **kwargs)
    return wrapper()
```

1. **Pas de `return` dans le wrapper.** La fonction d'origine s'exécute, mais son résultat est perdu — et la fonction décorée renvoie toujours `None`. La correction : `return func(*args, **kwargs)`.
2. **`return wrapper()` avec des parenthèses.** Cela **appelle** le wrapper dès la définition, et renvoie son résultat au lieu de la fonction. La correction : `return wrapper`.

**La règle :** le wrapper renvoie **le résultat**, et le décorateur renvoie **le wrapper**.

<!-- exercises:start — generated by scripts/intro6/sync_site_exercises.py; edit scripts/intro6/exercises/ -->
## Exercices

Les exercices n'utilisent que ce qui a été vu dans cette leçon et les précédentes. Essayez d'abord seul, et n'ouvrez la solution qu'ensuite.

### Exercice 1: Un premier décorateur

Écrivez un décorateur nommé `log_call` qui affiche `Calling <name>` avant chaque appel à la fonction, et appliquez-le avec `@` à `add` et à `multiply`.

```python
print(add(2, 3))
print(multiply(4, 5))
```

**Résultat attendu :**

```text
Calling add
5
Calling multiply
20
```

{{< solution >}}
```python
def log_call(func):
    def wrapper(a, b):
        print("Calling", func.__name__)
        return func(a, b)
    return wrapper


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


@log_call
def multiply(a, b):
    return a * b


print(add(2, 3))
print(multiply(4, 5))
```

```text
Calling add
5
Calling multiply
20
```

`@log_call` au-dessus de `def add` équivaut à `add = log_call(add)`. Désormais `add` pointe vers le wrapper.
{{< /solution >}}

### Exercice 2: Un wrapper adapté à n'importe quelle fonction

Le wrapper précédent ne fonctionne qu'avec des fonctions qui reçoivent exactement deux paramètres. Réécrivez `log_call` pour qu'il fonctionne avec n'importe quelle fonction.

```python
@log_call
def greet(name, greeting="Hello"):
    return greeting + ", " + name

@log_call
def total(*prices):
    return sum(prices)

print(greet("Dana", greeting="Hi"))
print(total(10, 20, 30))
```

**Résultat attendu :**

```text
Calling greet
Hi, Dana
Calling total
60
```

{{< solution >}}
```python
def log_call(func):
    def wrapper(*args, **kwargs):
        print("Calling", func.__name__)
        return func(*args, **kwargs)
    return wrapper


@log_call
def greet(name, greeting="Hello"):
    return greeting + ", " + name


@log_call
def total(*prices):
    return sum(prices)


print(greet("Dana", greeting="Hi"))
print(total(10, 20, 30))
```

```text
Calling greet
Hi, Dana
Calling total
60
```

`*args` et `**kwargs` rassemblent tous les arguments, et `func(*args, **kwargs)` les transmet tels quels.
{{< /solution >}}

### Exercice 3: Vérifier l'entrée

Écrivez un décorateur nommé `positive_only` pour des fonctions qui reçoivent un seul nombre : si le nombre est négatif, il lance une `ValueError` et n'appelle pas du tout la fonction. Le message contient le nom de la fonction (`func.__name__`) et le nombre reçu, comme dans le résultat attendu.

```python
@positive_only
def square_root(n):
    return n ** 0.5

print(square_root(16))
try:
    square_root(-4)
except ValueError as error:
    print("error:", error)
```

**Résultat attendu :**

```text
4.0
error: square_root needs a positive number, got -4
```

{{< solution >}}
```python
def positive_only(func):
    def wrapper(n):
        if n < 0:
            raise ValueError(func.__name__ + " needs a positive number, got " + str(n))
        return func(n)
    return wrapper


@positive_only
def square_root(n):
    return n ** 0.5


print(square_root(16))
try:
    square_root(-4)
except ValueError as error:
    print("error:", error)
```

```text
4.0
error: square_root needs a positive number, got -4
```

La vérification a lieu **avant** l'appel à `func` — si l'entrée est invalide, la fonction d'origine ne s'exécute pas du tout.
{{< /solution >}}

### Exercice 4: Compter les appels

Écrivez un décorateur nommé `count_calls`, qui compte combien de fois la fonction a été appelée. À chaque appel, **avant** l'exécution de la fonction, il affiche le nom de la fonction et le numéro de l'appel — comme dans le résultat attendu.

```python
@count_calls
def say_hi():
    print("Hi")

say_hi()
say_hi()
```

**Résultat attendu :**

```text
say_hi call #1
Hi
say_hi call #2
Hi
```

{{< solution >}}
```python
def count_calls(func):
    count = 0

    def wrapper(*args, **kwargs):
        nonlocal count
        count += 1
        print(func.__name__, "call #" + str(count))
        return func(*args, **kwargs)

    return wrapper


@count_calls
def say_hi():
    print("Hi")


say_hi()
say_hi()
```

```text
say_hi call #1
Hi
say_hi call #2
Hi
```

`count` appartient à `count_calls`, et le wrapper s'en souvient (une closure). Comme il **modifie** `count`, il a besoin de `nonlocal`.
{{< /solution >}}

### Exercice 5: Deux erreurs

```python
def shout(func):
    def wrapper(*args, **kwargs):
        result = func(*args, **kwargs)
        result.upper()
    return wrapper()
```

Le décorateur est censé renvoyer le résultat de la fonction en majuscules. Trouvez deux erreurs et corrigez-les.

```python
@shout
def greet(name):
    return "hello " + name

print(greet("dana"))
```

**Résultat attendu :**

```text
HELLO DANA
```

{{< solution >}}
1. **`return wrapper()` avec des parenthèses** — appelle le wrapper dès la définition, au lieu de le renvoyer. Il faut `return wrapper`.
2. **Le wrapper ne renvoie rien** — `result.upper()` crée une nouvelle chaîne, mais elle n'est pas renvoyée. Il faut `return result.upper()`.

```python
def shout(func):
    def wrapper(*args, **kwargs):
        result = func(*args, **kwargs)
        return result.upper()
    return wrapper


@shout
def greet(name):
    return "hello " + name


print(greet("dana"))
```

```text
HELLO DANA
```

La règle : le wrapper renvoie **le résultat**, et le décorateur renvoie **le wrapper**.
{{< /solution >}}

<!-- exercises:end -->

## Résumé

- **Décorateur** = une fonction qui reçoit une fonction, l'enveloppe dans un **wrapper** avec un comportement supplémentaire, et renvoie le wrapper.
- Il est construit à partir d'une **fonction d'ordre supérieur** et d'une **closure** — les deux sujets des leçons précédentes.
- `@decorator` au-dessus d'un `def` = `func = decorator(func)`.
- Avec `*args` et `**kwargs`, le wrapper s'adapte à n'importe quelle fonction.
- Le wrapper renvoie **le résultat** (`return func(...)`), et le décorateur renvoie **le wrapper** (`return wrapper`, sans parenthèses).

<!-- docs:start — generated by scripts/intro6/site_docs.py; edit the links there -->

## Documentation officielle

- [Python Glossary — decorator](https://docs.python.org/3/glossary.html#term-decorator)
- [Function definitions](https://docs.python.org/3/reference/compound_stmts.html#function-definitions)
- [functools.wraps](https://docs.python.org/3/library/functools.html#functools.wraps)

<!-- docs:end -->
