Semaine 4 : Les fonctions
🎯 Objectifs d'apprentissage
À la fin de cette semaine, vous serez capable de :
- Définir une fonction avec
def, des paramètres, et une valeur dereturn. - Expliquer la différence entre la valeur par défaut d'un paramètre et l'argument passé au moment de l'appel.
- Raisonner sur la portée (scope) des variables : ce qu'une fonction peut et ne peut pas voir depuis l'extérieur d'elle-même.
- Retourner plus d'une valeur depuis une fonction, et documenter ce que fait une fonction avec une docstring.
- Reconnaître la récursivité, et savoir quand elle reflète naturellement une définition mathématique.
Leçon
Les fonctions comme
Vous savez déjà lire comme « une règle qui prend un nombre et en renvoie un autre ». Une fonction Python, c'est exactement cela :
def f(x):
return x**2 + 1
f(3) # 10
f(0) # 1
def nomme la fonction et ses paramètres ; le corps calcule un résultat ; return renvoie ce résultat à l'appelant. Une fonction sans instruction return renvoie implicitement None.
C'est une bonne pratique de documenter ce que fait une fonction avec une docstring — une chaîne de caractères littérale placée juste après la ligne def, que les outils (et les autres programmeurs, y compris vous-même dans le futur) peuvent lire sans ouvrir le corps de la fonction :
def f(x):
"""Return x squared, plus one."""
return x**2 + 1
Plusieurs paramètres, valeurs par défaut
Les fonctions peuvent prendre plusieurs entrées, certaines avec des valeurs par défaut — l'équivalent de mais où a une valeur supposée si l'appelant n'en fournit pas :
def greet(name, greeting="Hello"):
return f"{greeting}, {name}!"
greet("Amina") # "Hello, Amina!"
greet("Youssef", "Hi") # "Hi, Youssef!"
greet(name="Sara", greeting="Hey") # arguments nommés (keyword arguments) — l'ordre n'a pas d'importance
Les paramètres avec une valeur par défaut doivent venir après les paramètres sans valeur par défaut — def greet(greeting="Hello", name): est une SyntaxError, car Python doit savoir quels arguments sont obligatoires avant de pouvoir déterminer lesquels sont optionnels.
Retourner plusieurs valeurs
Une fonction ne peut return qu'une seule chose, mais cette « seule chose » peut être un tuple — et la syntaxe de dépaquetage de tuples de Python (vue en semaine 3) fait que cela se lit presque comme un retour direct de plusieurs valeurs :
def min_and_max(numbers):
return min(numbers), max(numbers) # ceci construit un tuple : (min, max)
lowest, highest = min_and_max([4, 8, 1, 9, 3])
print(lowest, highest) # 1 9
C'est le même schéma que vous utiliserez tout au long du reste du cours chaque fois qu'un calcul produit naturellement plusieurs résultats liés.
La portée (scope) : ce qu'une fonction peut voir
Une variable créée à l'intérieur d'une fonction n'existe qu'à l'intérieur de celle-ci — c'est la portée locale (local scope) :
def compute():
total = 42
return total
compute()
print(total) # NameError : total n'existait qu'à l'intérieur de compute()
Une fonction peut lire des variables définies en dehors d'elle (portée globale, global scope), mais réaffecter une variable globale depuis l'intérieur d'une fonction sans syntaxe particulière crée à la place une nouvelle variable locale — une source fréquente de confusion :
counter = 0
def increment():
counter = counter + 1 # UnboundLocalError ! Python voit l'affectation
# et traite counter comme local *dans toute la fonction*,
# y compris lors de la lecture avant l'affectation.
La solution (global counter) existe mais est rarement le bon choix de conception — préférez passer les valeurs en entrée et retourner les résultats en sortie, ce qui est aussi plus facile à tester et à raisonner.
Des fonctions qui appellent des fonctions
Comme une fonction n'est jamais qu'une valeur comme une autre, les fonctions peuvent appeler d'autres fonctions, construisant de la complexité à partir de petites briques — de la même manière que compose deux fonctions :
def double(x):
return x * 2
def add_one(x):
return x + 1
def double_then_add_one(x):
return add_one(double(x))
double_then_add_one(3) # double(3)=6, add_one(6)=7
La récursivité : une fonction qui s'appelle elle-même
Certaines fonctions se définissent le plus naturellement en termes d'elles-mêmes — exactement comme une relation de récurrence. La factorielle, avec le cas de base , se traduit presque mot pour mot :
def factorial(n):
if n == 0:
return 1 # cas de base — arrête la récursivité
return n * factorial(n - 1) # cas récursif
factorial(5) # 5 * factorial(4) = 5 * 4 * factorial(3) = ... = 120
Toute fonction récursive a besoin d'un cas de base qui ne s'appelle pas lui-même (sinon elle se répète indéfiniment, jusqu'à finir par planter avec une RecursionError) et d'un cas récursif qui se rapproche de ce cas de base à chaque appel — ici, n - 1 se réduit vers 0 à chaque fois. Tout ce que la récursivité peut faire, une boucle peut aussi le faire (et souvent plus efficacement, car chaque appel récursif a un certain coût) — mais pour des définitions déjà naturellement récursives, comme la factorielle ou le pipeline de traitement CSV de la semaine 5, la version récursive peut être la plus claire à lire.
⚠️ Pièges courants
- Oublier
return. Une fonction qui calcule une valeur mais ne lareturne jamais renvoieNone—result = add(2, 3)devient silencieusementNonesiadda oublié son instructionreturn, et le bug ne se manifeste souvent que bien plus tard, lorsque vous essayez d'utiliserresult. - Utiliser un argument par défaut mutable.
def add_item(item, items=[]):semble raisonnable, mais cette liste par défaut n'est créée qu'une seule fois, au moment où la fonction est définie, et réutilisée à chaque appel qui ne fournit pas son propreitems— des éléments d'un appel peuvent fuiter vers le suivant. La solution : mettreNonepar défaut et créer une liste fraîche à l'intérieur de la fonction si besoin. - Absence de cas de base dans une fonction récursive. Oublier le
if n == 0: return 1dansfactorialsignifie que chaque appel se répète encore et encore, sans fin, jusqu'à ce que Python abandonne avec uneRecursionError: maximum recursion depth exceeded. - Masquer un nom prédéfini (built-in). Nommer votre propre fonction
sumoulistfonctionne, mais cela masque ensuite le véritablesum()/list()de Python pour le reste de ce fichier — un bug confus à traquer plus tard.
🧩 Défis
Écrivez une fonction is_even(n) qui renvoie True si n est pair, False sinon.
Écrivez une fonction average(numbers) qui prend une liste de nombres et renvoie leur moyenne. Que se passe-t-il si vous l'appelez avec une liste vide ?
Transformez la logique « ce nombre est-il premier ? » de la semaine dernière en une fonction is_prime(n), puis utilisez-la dans une compréhension de liste pour construire une liste de tous les nombres premiers inférieurs à 50.
Écrivez une fonction f(x, y=1) qui renvoie x + y. Appelez-la une fois avec seulement x, puis une fois avec les deux arguments, et expliquez pourquoi les deux résultats diffèrent.
Écrivez une fonction récursive fibonacci(n) qui renvoie le n-ième nombre de Fibonacci, en utilisant la définition , , .
Écrivez une fonction stats(numbers) qui renvoie trois valeurs à la fois — le minimum, le maximum et la moyenne — et appelez-la en utilisant le dépaquetage de tuples pour capturer les trois dans des variables séparées.
🤔 Questions socratiques
- Deux fonctions utilisent chacune en interne une variable nommée
total. Interfèrent-elles l'une avec l'autre ? Pourquoi, ou pourquoi pas, au vu de ce que vous avez appris sur la portée ? - Pourquoi
returnquitte-t-elle immédiatement une fonction, même s'il reste du code après elle ? Essayez d'écrire une fonction avec du code inaccessible après unreturnet observez ce que fait le bac à sable (playground) avec ça. double_then_add_onecomposedoubleetadd_one. Pourriez-vous écrire une fonction généralecompose(f, g)qui renvoie une nouvelle fonction combinant deux fonctions quelconques ? (Vous n'avez pas besoin declasspour cela — une fonction peut renvoyer une autre fonction.)- Votre
factorialrécursive et une version itérative utilisant une boucleforcalculent le même résultat. Essayez de chronométrer les deux sur une grande entrée (par exemplefactorial(900)) — remarquez-vous une différence ? Que pensez-vous qu'il se passe à chaque appel récursif qu'une itération de boucle n'a pas besoin de faire ? - Le piège de l'argument par défaut mutable se produit parce qu'une valeur par défaut est créée une seule fois, au moment de la définition de la fonction, et non recréée à chaque appel. Pourquoi Python a-t-il pu être conçu ainsi, plutôt que de recréer la valeur par défaut à chaque appel (ce qui éviterait le piège mais coûterait un peu plus de travail à chaque fois) ?
✅ Quiz hebdomadaire
✅ Quiz de la semaine
🎁 Bonus : gérer les erreurs avec try/except
Disponible une fois le quiz de la semaine réussi.