# Les fonctions pures et les effets de bord<no value>

## Qu'allons-nous apprendre ?

- Ce qu'est une **fonction pure** (Pure Function).
- Ce qu'est un **effet de bord** (Side Effect) — et quels types existent.
- Le piège de **la modification d'une liste reçue en paramètre**.
- Comment écrire une **version pure** d'une fonction.

## Pourquoi en avons-nous besoin ?

Voici une fonction qui semble innocente :

```python
total = 0

def add_to_total(price):
    global total
    total += price
    return total

print(add_to_total(10))
print(add_to_total(10))
```

**Sortie :**

```text
10
20
```

**Exactement le même appel — et un résultat différent.** La fonction dépend d'une variable extérieure et la modifie en plus. Pour savoir ce qu'elle va renvoyer, il faut connaître tout l'historique du programme. Elle est difficile à tester, et difficile à croire.

## Explication à partir des bases

### Ce qu'est une fonction pure

Une **fonction pure** remplit deux conditions :

1. **La même entrée → toujours le même résultat.** Le résultat ne dépend que des paramètres — pas de variables globales, pas de l'heure, pas d'une saisie de l'utilisateur.
2. **Pas d'effets de bord.** Elle ne modifie rien à l'extérieur. Tout ce qu'elle « produit » sort par `return`.

### Ce qui compte comme effet de bord

| Effet de bord | Exemple |
|---|---|
| Modifier une variable globale | `global total` puis `total += price` |
| Modifier une liste ou un dictionnaire reçu | `prices[i] = ...` sur la liste reçue en paramètre |
| Les entrées et sorties avec l'utilisateur | `print(...)`, `input(...)` |
| Écrire dans un fichier ou une base de données | `open(..., "w")`, `INSERT` |

{{< box "analogy" "Analogie : une machine à jus" >}}
Une fonction pure est comme un presse-agrumes : vous mettez 3 oranges — un verre de jus sort. Toujours la même quantité, et le presse-agrumes ne touche à rien d'autre dans la cuisine. Une fonction impure est un presse-agrumes qui déplace parfois aussi le réfrigérateur — et soudain quelque chose d'autre dans la maison ne marche plus, sans que vous sachiez pourquoi.
{{< /box >}}

## Syntaxe

Il n'y a pas de syntaxe spéciale — c'est **une façon d'écrire** les fonctions :

```python
# pure: result depends only on the parameters
def add_price(total, price):
    return total + price

# not pure: reads and changes something outside
def add_to_total(price):
    global total
    total += price
    return total
```

## Premier exemple de code

La même fonction que celle du début de la leçon, en version pure. `week-2/pure_total.py` :

```python
def add_price(total, price):
    return total + price

total = 0
total = add_price(total, 10)
total = add_price(total, 10)
print(total)

print(add_price(0, 10))
print(add_price(0, 10))
```

**Sortie :**

```text
20
10
10
```

## Analyse du code ligne par ligne

| Ligne | Explication |
|---|---|
| `def add_price(total, price):` | Tout ce dont la fonction a besoin arrive **par les paramètres**. |
| `return total + price` | Elle ne modifie rien — elle ne fait que calculer et renvoyer. |
| `total = add_price(total, 10)` | **L'appelant** décide quoi faire du résultat : ici — le stocker dans `total`. |
| `print(add_price(0, 10))` deux fois | La même entrée → le même résultat, `10`, à chaque fois. |

## Un autre exemple

**Le piège : modifier une liste reçue.** `week-2/discount_trap.py` :

```python
def add_discount(prices):
    for i in range(len(prices)):
        prices[i] = prices[i] * 0.9
    return prices

original = [100, 200]
discounted = add_discount(original)
print(discounted)
print(original)
```

**Sortie :**

```text
[90.0, 180.0]
[90.0, 180.0]
```

**`original` a changé !** Comme nous l'avons appris dans la leçon sur la mutabilité, une liste est passée à une fonction **par référence** — `prices` et `original` sont deux noms pour la même liste. Celui qui utilisait `original` plus loin dans le programme reçoit soudain des prix différents.

**La version pure — construire une nouvelle liste :**

```python
def add_discount(prices):
    result = []
    for price in prices:
        result.append(price * 0.9)
    return result

original = [100, 200]
discounted = add_discount(original)
print(discounted)
print(original)
```

**Sortie :**

```text
[90.0, 180.0]
[100, 200]
```

Maintenant, `original` reste exactement tel qu'il était, et `discounted` est une liste entièrement nouvelle.

## Erreur courante

**L'erreur : croire que toute fonction doit être pure.** Un programme qui n'affiche rien, n'enregistre rien dans un fichier et n'écrit rien dans une base de données — ne fait rien d'utile. Les effets de bord sont **une partie nécessaire** de tout programme.

La règle est de **séparer** :

- **Le calcul** — dans des fonctions pures (`calculate_total`, `apply_discount`).
- **La communication avec le monde** — à quelques endroits bien identifiés (`print`, l'enregistrement dans un fichier).

```python
def calculate_total(prices):        # pure: easy to test
    return sum(prices)

def show_total(prices):             # side effect: printing
    print("Total:", calculate_total(prices))
```

<!-- 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: Pure ou non ?

Pour chaque fonction — est-elle pure ? Sinon, quel est l'effet de bord ?

```text
def square(n):
    return n * n

def log_square(n):
    print("squaring", n)
    return n * n

def add_item(cart, item):
    cart.append(item)
    return cart

def with_item(cart, item):
    return cart + [item]
```

{{< solution >}}
| Fonction | Pure ? | Pourquoi |
|---|---|---|
| `square` | **Oui** | Le résultat ne dépend que de `n`, et rien ne change |
| `log_square` | **Non** | `print` est un effet de bord — une sortie à l'écran |
| `add_item` | **Non** | `append` modifie la liste reçue (Mutation) |
| `with_item` | **Oui** | `cart + [item]` crée une **nouvelle** liste, et l'original n'a pas été touché |
{{< /solution >}}

### Exercice 2: Le même appel — un résultat différent

```python
total = 0

def add_to_total(price):
    global total
    total += price
    return total
```

Le problème : chaque appel à `add_to_total(10)` renvoie un résultat différent (10, puis 20, puis 30), car la fonction dépend d'une variable globale et la modifie.

Réécrivez la fonction en version pure nommée `add_price(total, price)` — elle reçoit le total actuel en paramètre et renvoie le nouveau total, sans `global`. Lancez le code de test, qui montre que la même entrée donne toujours le même résultat :

```python
total = 0
total = add_price(total, 10)
total = add_price(total, 10)
print(total)

print(add_price(0, 10))
print(add_price(0, 10))
```

**Résultat attendu :**

```text
20
10
10
```

{{< solution >}}
```python
def add_price(total, price):
    return total + price


total = 0
total = add_price(total, 10)
total = add_price(total, 10)
print(total)

print(add_price(0, 10))
print(add_price(0, 10))
```

```text
20
10
10
```

Tout ce dont la fonction a besoin arrive **par les paramètres**, et le résultat ne sort que par `return`. L'appelant décide quoi en faire.
{{< /solution >}}

### Exercice 3: Ne modifiez pas la liste reçue

```python
TAX = 1.17

def apply_tax(prices):
    for i in range(len(prices)):
        prices[i] = prices[i] * TAX
```

Le problème : la fonction modifie la liste reçue, donc après l'appel les prix d'origine sont perdus.

Écrivez une version pure de `apply_tax(prices)`, qui renvoie une **nouvelle** liste avec les prix taxes comprises (chaque prix arrondi à deux chiffres) et ne touche pas à la liste reçue. Lancez le code de test — la première ligne prouve que la liste d'origine n'a pas changé :

```python
prices = [100, 50]
with_tax = apply_tax(prices)
print(prices)
print(with_tax)
```

**Résultat attendu :**

```text
[100, 50]
[117.0, 58.5]
```

{{< solution >}}
```python
TAX = 1.17


def apply_tax(prices):
    result = []
    for price in prices:
        result.append(round(price * TAX, 2))
    return result


prices = [100, 50]
with_tax = apply_tax(prices)
print(prices)
print(with_tax)
```

```text
[100, 50]
[117.0, 58.5]
```

`TAX` est une constante qui ne change pas, la lire ne nuit donc pas à la pureté — la même entrée donne toujours le même résultat.
{{< /solution >}}

### Exercice 4: Séparer le calcul de l'affichage

```python
def show_total(prices):
    total = 0
    for price in prices:
        total = total + price
    print("Total:", total)
```

La fonction calcule et affiche à la fois. Découpez-la en deux :

- `calculate_total(prices)` — pure : ne fait que calculer et renvoyer la somme ;
- `show_total(prices)` — appelle `calculate_total` et affiche le résultat.

Code de test :

```python
show_total([10, 20, 30])
```

**Résultat attendu :**

```text
Total: 60
```

{{< solution >}}
```python
def calculate_total(prices):
    return sum(prices)


def show_total(prices):
    print("Total:", calculate_total(prices))


show_total([10, 20, 30])
```

```text
Total: 60
```

Le calcul (`calculate_total`) est pur et facile à tester. L'effet de bord (`print`) est concentré à un seul endroit bien identifié.
{{< /solution >}}

### Exercice 5: Les trois meilleurs — sans rien modifier

Écrivez une fonction pure `top_three(numbers)` qui renvoie les trois plus grands nombres, du plus grand au plus petit, sans modifier la liste d'origine. Affichez aussi la liste d'origine pour prouver qu'elle n'a pas changé.

```python
scores = [70, 95, 88, 60, 99]
print(top_three(scores))
print(scores)
```

**Résultat attendu :**

```text
[99, 95, 88]
[70, 95, 88, 60, 99]
```

{{< solution >}}
```python
def top_three(numbers):
    return sorted(numbers, reverse=True)[:3]


scores = [70, 95, 88, 60, 99]
print(top_three(scores))
print(scores)
```

```text
[99, 95, 88]
[70, 95, 88, 60, 99]
```

`sorted` renvoie une **nouvelle** liste triée, et le Slicing (`[:3]`) renvoie encore une nouvelle liste — la liste d'origine n'a pas été touchée, la fonction est donc pure.
{{< /solution >}}

<!-- exercises:end -->

## Résumé

- **Fonction pure** = la même entrée → toujours le même résultat, et sans rien modifier à l'extérieur.
- **Effets de bord** : modifier une globale, modifier une liste ou un dictionnaire reçu, `print`/`input`, écrire dans un fichier ou une base de données.
- Une liste est passée par référence — une fonction qui la modifie modifie l'**original**. La version pure construit une nouvelle liste.
- Les fonctions pures sont faciles à tester, prévisibles, et faciles à réutiliser.
- Tout ne peut pas être pur — séparez le **calcul** pur de la **communication avec le monde**.

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

## Documentation officielle

- [Functional Programming HOWTO](https://docs.python.org/3/howto/functional.html#introduction)

<!-- docs:end -->
