Les fonctions pures et les effets de bord
- Expliquer ce qu’est une fonction pure (Pure Function) — la même entrée, toujours le même résultat, sans rien modifier à l’extérieur
- Reconnaître les effets de bord dans du code — global, modifier une liste reçue, afficher, écrire dans un fichier
- Transformer une fonction qui modifie son entrée en une version pure qui renvoie une nouvelle valeur
- Expliquer pourquoi les fonctions pures sont plus faciles à tester et à réutiliser
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 :
total = 0
def add_to_total(price):
global total
total += price
return total
print(add_to_total(10))
print(add_to_total(10))Sortie :
10
20Exactement 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 :
- 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.
- 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 |
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.
Syntaxe
Il n’y a pas de syntaxe spéciale — c’est une façon d’écrire les fonctions :
# 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 totalPremier exemple de code
La même fonction que celle du début de la leçon, en version pure. week-2/pure_total.py :
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 :
20
10
10Analyse 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 :
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 :
[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 :
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 :
[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).
def calculate_total(prices): # pure: easy to test
return sum(prices)
def show_total(prices): # side effect: printing
print("Total:", calculate_total(prices))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 ?
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]Afficher la 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é |
Exercice 2: Le même appel — un résultat différent
total = 0
def add_to_total(price):
global total
total += price
return totalLe 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 :
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 :
20
10
10Afficher la solution
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))20
10
10Tout 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.
Exercice 3: Ne modifiez pas la liste reçue
TAX = 1.17
def apply_tax(prices):
for i in range(len(prices)):
prices[i] = prices[i] * TAXLe 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é :
prices = [100, 50]
with_tax = apply_tax(prices)
print(prices)
print(with_tax)Résultat attendu :
[100, 50]
[117.0, 58.5]Afficher la solution
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)[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.
Exercice 4: Séparer le calcul de l’affichage
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)— appellecalculate_totalet affiche le résultat.
Code de test :
show_total([10, 20, 30])Résultat attendu :
Total: 60Afficher la solution
def calculate_total(prices):
return sum(prices)
def show_total(prices):
print("Total:", calculate_total(prices))
show_total([10, 20, 30])Total: 60Le calcul (calculate_total) est pur et facile à tester. L’effet de bord (print) est concentré à un seul endroit bien identifié.
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é.
scores = [70, 95, 88, 60, 99]
print(top_three(scores))
print(scores)Résultat attendu :
[99, 95, 88]
[70, 95, 88, 60, 99]Afficher la solution
def top_three(numbers):
return sorted(numbers, reverse=True)[:3]
scores = [70, 95, 88, 60, 99]
print(top_three(scores))
print(scores)[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.
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.