Les fonctions pures et les effets de bord

Objectifs de la leçon
  • 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
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 bordExemple
Modifier une variable globaleglobal total puis total += price
Modifier une liste ou un dictionnaire reçuprices[i] = ... sur la liste reçue en paramètre
Les entrées et sorties avec l’utilisateurprint(...), input(...)
Écrire dans un fichier ou une base de donnéesopen(..., "w"), INSERT
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.

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 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 :

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
10

Analyse du code ligne par ligne

LigneExplication
def add_price(total, price):Tout ce dont la fonction a besoin arrive par les paramètres.
return total + priceElle 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 foisLa 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
FonctionPure ?Pourquoi
squareOuiLe résultat ne dépend que de n, et rien ne change
log_squareNonprint est un effet de bord — une sortie à l’écran
add_itemNonappend modifie la liste reçue (Mutation)
with_itemOuicart + [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 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 :

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
10
Afficher 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
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.

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] * 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é :

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) — appelle calculate_total et affiche le résultat.

Code de test :

show_total([10, 20, 30])

Résultat attendu :

Total: 60
Afficher la solution
def calculate_total(prices):
    return sum(prices)


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


show_total([10, 20, 30])
Total: 60

Le 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.

Documentation officielle