# Chapitre 05 — Les fonctions > **Durée estimée :** 5 à 6 heures (cours, exercices et mini-projet) > > **Prérequis :** [Chapitre 04 — Les structures de données](04-structures-de-donnees.md) (listes, tuples, dictionnaires, ensembles) > > **Objectifs — à la fin de ce chapitre, tu sauras…** > > - définir une fonction avec `def` et l'appeler ; > - transmettre des informations avec des **paramètres** et récupérer un résultat avec `return` ; > - utiliser des valeurs par défaut, des arguments nommés, `*args` et `**kwargs` ; > - comprendre la **portée** des variables (locale ou globale) ; > - documenter une fonction avec une **docstring** ; > - écrire une fonction **récursive** simple et découper un programme en fonctions. --- ## Cours ### 1. Pourquoi des fonctions ? Tu utilises déjà des fonctions depuis le début : `print()`, `len()`, `input()`, `sum()`… Ce sont des **morceaux de code prêts à l'emploi**, auxquels on donne des informations et qui font un travail. Imagine maintenant que, dans ton programme, tu doives afficher un joli cadre autour d'un titre à cinq endroits différents. Tu pourrais copier-coller cinq fois les mêmes lignes… mais le jour où tu veux changer le style du cadre, il faudra modifier les cinq copies (et tu en oublieras une). **Une fonction, c'est une recette de cuisine :** tu l'écris une fois, tu lui donnes un nom (« pâte à crêpes »), et ensuite il suffit de dire « fais une pâte à crêpes » pour qu'elle soit exécutée. Les ingrédients sont les **paramètres**, le plat obtenu est la **valeur de retour**. Les fonctions permettent de : - **ne pas se répéter** (principe *DRY* : *Don't Repeat Yourself*) ; - **découper** un gros problème en petits problèmes simples ; - **nommer** une action, ce qui rend le code lisible comme un texte ; - **tester** chaque morceau séparément (on le fera au [chapitre 13](13-tests-pytest-bonnes-pratiques.md)). --- ### 2. Définir et appeler une fonction On définit une fonction avec le mot-clé `def`, suivi de son nom, de parenthèses et de deux-points. Le corps de la fonction est **indenté**, comme pour un `if` ou un `for`. ```python def dire_bonjour(): print("Bonjour !") print("Bienvenue dans le cours Python.") ``` Si tu exécutes uniquement ce code, **rien ne s'affiche** ! On a seulement écrit la recette, on ne l'a pas encore cuisinée. Pour exécuter la fonction, il faut l'**appeler** en écrivant son nom suivi de parenthèses : ```python def dire_bonjour(): print("Bonjour !") print("Bienvenue dans le cours Python.") dire_bonjour() dire_bonjour() ``` ```text Bonjour ! Bienvenue dans le cours Python. Bonjour ! Bienvenue dans le cours Python. ``` Quelques règles : - Le nom suit les mêmes règles que les variables : minuscules, mots séparés par `_`. Choisis de préférence un **verbe** qui décrit l'action : `calculer_moyenne`, `afficher_menu`, `demander_age`. - La fonction doit être **définie avant d'être appelée** (plus haut dans le fichier). - Par convention (PEP 8, voir le [chapitre 13](13-tests-pytest-bonnes-pratiques.md)), on laisse **deux lignes vides** avant et après une définition de fonction. Si tu appelles une fonction avant de l'avoir définie : ```python saluer() def saluer(): print("Salut") ``` ```text Traceback (most recent call last): File "", line 1, in NameError: name 'saluer' is not defined ``` Python lit ton fichier de haut en bas : au moment de l'appel, il ne connaît pas encore `saluer`. --- ### 3. Les paramètres : donner des informations à la fonction Une fonction devient vraiment utile quand on peut lui transmettre des données. On déclare des **paramètres** entre les parenthèses : ce sont des variables qui recevront les valeurs au moment de l'appel. ```python def saluer(prenom): print(f"Bonjour {prenom} !") saluer("Alice") saluer("Bob") ``` ```text Bonjour Alice ! Bonjour Bob ! ``` Vocabulaire : - **paramètre** : le nom dans la définition (`prenom`) ; - **argument** : la valeur effectivement transmise lors de l'appel (`"Alice"`). On peut avoir plusieurs paramètres, séparés par des virgules : ```python def afficher_produit(nom, prix, quantite): total = prix * quantite print(f"{quantite} x {nom} à {prix:.2f} € = {total:.2f} €") afficher_produit("Croissant", 1.10, 4) afficher_produit("Baguette", 1.25, 2) ``` ```text 4 x Croissant à 1.10 € = 4.40 € 2 x Baguette à 1.25 € = 2.50 € ``` Les arguments sont associés aux paramètres **dans l'ordre** : le premier argument va dans le premier paramètre, etc. C'est pourquoi on les appelle des **arguments positionnels**. Si tu en donnes trop ou pas assez, Python proteste : ```python def saluer(prenom): print(f"Bonjour {prenom} !") saluer() ``` ```text Traceback (most recent call last): File "", line 5, in TypeError: saluer() missing 1 required positional argument: 'prenom' ``` --- ### 4. `return` : renvoyer un résultat Jusqu'ici, nos fonctions **affichent** quelque chose. Mais souvent, on veut qu'une fonction **calcule** une valeur et nous la **donne** pour qu'on puisse s'en servir ensuite. C'est le rôle de `return`. ```python def carre(nombre): return nombre * nombre resultat = carre(5) print(resultat) # → 25 print(carre(3) + carre(4)) # → 25 ``` L'appel `carre(5)` est **remplacé** par la valeur renvoyée, exactement comme `len([1, 2, 3])` est remplacé par `3`. On peut donc l'utiliser dans un calcul, le stocker, le passer à une autre fonction… #### `print` ou `return` ? La grande confusion C'est LA confusion du débutant. Compare : ```python def double_affiche(x): print(x * 2) def double_renvoie(x): return x * 2 a = double_affiche(10) # affiche 20, mais... b = double_renvoie(10) # n'affiche rien, mais... print(a) # ... a vaut None ! print(b) # ... b vaut bien 20 ``` ```text 20 None 20 ``` - `print` **montre** une valeur à l'humain qui regarde l'écran. Le programme, lui, ne peut rien en faire. - `return` **donne** une valeur au programme, qui peut la réutiliser. **Analogie :** demander à un collègue de calculer un total. Avec `print`, il crie le résultat dans le couloir : tu l'entends, mais tu ne l'as pas sur papier. Avec `return`, il te tend une feuille avec le résultat : tu peux la garder et t'en servir. **Règle d'or :** une fonction qui **calcule** doit **renvoyer** son résultat ; c'est le code qui l'appelle qui décide de l'afficher ou non. #### Une fonction sans `return` renvoie `None` Si une fonction se termine sans rencontrer de `return`, elle renvoie automatiquement `None`, la valeur qui signifie « rien ». #### `return` arrête la fonction immédiatement Dès qu'un `return` est exécuté, la fonction s'arrête ; les lignes suivantes sont ignorées. On s'en sert souvent pour sortir tôt : ```python def est_majeur(age): if age >= 18: return True return False print(est_majeur(20)) # → True print(est_majeur(15)) # → False ``` On peut d'ailleurs écrire plus directement `return age >= 18`, puisque la comparaison est déjà un booléen. ```python def mention(note): if note >= 16: return "Très bien" if note >= 14: return "Bien" if note >= 12: return "Assez bien" if note >= 10: return "Passable" return "Insuffisant" for note in [17, 14.5, 10, 8]: print(note, "→", mention(note)) ``` ```text 17 → Très bien 14.5 → Bien 10 → Passable 8 → Insuffisant ``` Pas besoin de `elif` ici : si la première condition est vraie, le `return` a déjà quitté la fonction. #### Renvoyer plusieurs valeurs Une fonction peut renvoyer plusieurs valeurs séparées par des virgules : elle renvoie en fait un **tuple**, qu'on déballe à l'arrivée (cf. [chapitre 04](04-structures-de-donnees.md)). ```python def min_max_moyenne(nombres): moyenne = sum(nombres) / len(nombres) return min(nombres), max(nombres), moyenne petit, grand, moy = min_max_moyenne([12, 8, 15, 19]) print(petit, grand, moy) # → 8 19 13.5 ``` --- ### 5. Valeurs par défaut et arguments nommés #### Les valeurs par défaut On peut donner une **valeur par défaut** à un paramètre : s'il n'est pas fourni à l'appel, c'est elle qui sera utilisée. ```python def saluer(prenom, salutation="Bonjour"): print(f"{salutation} {prenom} !") saluer("Alice") # → Bonjour Alice ! saluer("Bob", "Salut") # → Salut Bob ! ``` Tu connais déjà ce mécanisme : `print()` a un paramètre `end` qui vaut `"\n"` par défaut, et `sorted()` a `reverse=False`. Les paramètres avec valeur par défaut doivent être placés **après** ceux qui n'en ont pas. `def f(a=1, b):` provoque une `SyntaxError`. #### Les arguments nommés À l'appel, on peut préciser le **nom** du paramètre : `nom=valeur`. L'ordre n'a alors plus d'importance, et surtout, le code devient beaucoup plus clair. ```python def creer_compte(pseudo, age, pays="France", newsletter=False): print(f"{pseudo}, {age} ans, {pays}, newsletter : {newsletter}") creer_compte("alice42", 25) creer_compte("bob_du_75", 31, newsletter=True) creer_compte(age=19, pseudo="chloe", pays="Belgique") ``` ```text alice42, 25 ans, France, newsletter : False bob_du_75, 31 ans, France, newsletter : True chloe, 19 ans, Belgique, newsletter : False ``` Compare `creer_compte("bob_du_75", 31, "France", True)` et `creer_compte("bob_du_75", 31, newsletter=True)` : dans le second cas, on comprend immédiatement ce que signifie `True`. Les arguments positionnels doivent toujours venir **avant** les arguments nommés à l'appel. --- ### 6. `*args` et `**kwargs` : un nombre variable d'arguments #### `*args` : autant d'arguments positionnels qu'on veut Comment `print()` peut-elle accepter 1, 2 ou 10 arguments ? Grâce à l'étoile `*`. Un paramètre précédé de `*` récupère **tous les arguments positionnels restants** dans un **tuple**. ```python def moyenne(*nombres): print(f"Reçu : {nombres}") if len(nombres) == 0: return 0 return sum(nombres) / len(nombres) print(moyenne(12, 15)) print(moyenne(8, 10, 12, 14)) print(moyenne()) ``` ```text Reçu : (12, 15) 13.5 Reçu : (8, 10, 12, 14) 11.0 Reçu : () 0 ``` Le nom `args` est une simple convention (*arguments*) ; c'est l'étoile qui compte. Ici, `nombres` est plus parlant. #### `**kwargs` : autant d'arguments nommés qu'on veut Deux étoiles `**` récupèrent tous les **arguments nommés** non prévus dans un **dictionnaire** (*kwargs* = *keyword arguments*). ```python def fiche(nom, **infos): print(f"--- {nom} ---") for cle, valeur in infos.items(): print(f"{cle} : {valeur}") fiche("Alice", age=30, ville="Lyon", metier="développeuse") fiche("Bob") ``` ```text --- Alice --- age : 30 ville : Lyon metier : développeuse --- Bob --- ``` #### L'étoile à l'appel : déballer une collection Inversement, à l'**appel**, `*` déballe une liste en arguments positionnels, et `**` déballe un dictionnaire en arguments nommés : ```python def addition(a, b, c): return a + b + c valeurs = [1, 2, 3] print(addition(*valeurs)) # → 6 (équivaut à addition(1, 2, 3)) options = {"b": 20, "c": 30} print(addition(10, **options)) # → 60 (équivaut à addition(10, b=20, c=30)) ``` L'ordre complet des paramètres dans une définition est : `def f(positionnels, avec_defaut=..., *args, **kwargs)`. Tu n'auras pas souvent besoin de tout combiner : `*args` et `**kwargs` sont surtout utiles pour écrire des fonctions très souples, et tu les croiseras beaucoup en lisant le code des bibliothèques. --- ### 7. La portée des variables : locale ou globale Une variable créée **à l'intérieur** d'une fonction est **locale** : elle n'existe que pendant l'exécution de la fonction, et n'est pas visible à l'extérieur. ```python def calculer(): resultat = 42 print(f"Dans la fonction : {resultat}") calculer() print(resultat) ``` ```text Dans la fonction : 42 Traceback (most recent call last): File "", line 6, in NameError: name 'resultat' is not defined ``` **Analogie :** une fonction est comme une pièce fermée avec un tableau blanc. Ce qu'on écrit sur le tableau à l'intérieur (les variables locales) est effacé en sortant. Pour faire sortir une information, il faut la passer par la porte : c'est `return`. Les paramètres sont eux aussi des variables locales. #### Lire une variable globale Une variable créée au niveau principal du fichier (hors de toute fonction) est **globale**. Une fonction peut la **lire** : ```python TAUX_TVA = 0.20 def prix_ttc(prix_ht): return prix_ht * (1 + TAUX_TVA) print(prix_ttc(100)) # → 120.0 ``` Les constantes globales (valeurs qui ne changent jamais) s'écrivent par convention en **MAJUSCULES**. #### Une affectation crée une variable locale En revanche, si tu **affectes** une variable dans une fonction, Python crée une nouvelle variable locale, même si une globale du même nom existe : ```python compteur = 0 def incrementer(): compteur = 100 # variable LOCALE, sans rapport avec la globale print(f"Dans la fonction : {compteur}") incrementer() print(f"Dehors : {compteur}") ``` ```text Dans la fonction : 100 Dehors : 0 ``` Et si tu essaies de modifier la globale avec `+=`, c'est l'erreur : ```python compteur = 0 def incrementer(): compteur += 1 incrementer() ``` ```text Traceback (most recent call last): File "", line 8, in File "", line 5, in incrementer UnboundLocalError: cannot access local variable 'compteur' where it is not associated with a value ``` Python a vu une affectation (`+=`) : il considère donc `compteur` comme locale… mais elle n'a pas encore de valeur. #### Le mot-clé `global` (à éviter) Le mot-clé `global` permet de dire « je veux modifier la variable globale » : ```python compteur = 0 def incrementer(): global compteur compteur += 1 incrementer() incrementer() print(compteur) # → 2 ``` Ça marche, mais **c'est une mauvaise habitude** : n'importe quelle fonction peut modifier la variable, et on ne sait plus qui a fait quoi. La bonne pratique : **passer les données en paramètres et récupérer les résultats avec `return`**. ```python def incrementer(valeur): return valeur + 1 compteur = 0 compteur = incrementer(compteur) compteur = incrementer(compteur) print(compteur) # → 2 ``` #### Les objets modifiables passés en paramètre Attention : si tu passes une **liste** ou un **dictionnaire** à une fonction, la fonction reçoit une étiquette vers **le même objet** (souviens-toi de la section « Références et copies » du chapitre 04). Si elle le modifie, la modification est visible à l'extérieur : ```python def ajouter_bonus(notes): notes.append(20) mes_notes = [12, 15] ajouter_bonus(mes_notes) print(mes_notes) # → [12, 15, 20] ``` Parfois c'est voulu (comme ici), parfois c'est un bug. Si la fonction ne doit pas toucher à l'original, elle doit travailler sur une copie ou construire une nouvelle liste qu'elle renvoie. --- ### 8. Documenter : les docstrings Une **docstring** est une chaîne entre triples guillemets placée **juste sous la ligne `def`**. Elle explique ce que fait la fonction, ce qu'elle attend et ce qu'elle renvoie. ```python def convertir_temperature(celsius): """Convertit une température de degrés Celsius en degrés Fahrenheit. Paramètre : celsius : la température en °C (int ou float). Renvoie : La température en °F (float). """ return celsius * 9 / 5 + 32 print(convertir_temperature(100)) # → 212.0 print(convertir_temperature(-40)) # → -40.0 ``` Contrairement à un commentaire `#`, la docstring est **attachée à la fonction** : la fonction `help()` l'affiche, et ton éditeur (VS Code) la montre quand tu survoles le nom de la fonction. ```python def carre(nombre): """Renvoie le carré de nombre.""" return nombre * nombre help(carre) ``` ```text Help on function carre in module __main__: carre(nombre) Renvoie le carré de nombre. ``` Pour une fonction simple, une seule ligne suffit. Pour une fonction plus complexe, décris les paramètres et la valeur de retour. Une bonne docstring dit **ce que fait** la fonction, pas **comment** elle le fait. > **Bonus :** au [chapitre 11](11-poo-avancee-heritage-typage.md), tu verras les **annotations de type** (`def carre(nombre: float) -> float:`), qui complètent la documentation. --- ### 9. Les fonctions sont des valeurs En Python, une fonction est un objet comme un autre : on peut la ranger dans une variable, dans une liste, ou la passer en argument à une autre fonction. Attention à la différence entre `carre` (la fonction elle-même) et `carre(3)` (le résultat de son appel). ```python def carre(x): return x * x def cube(x): return x * x * x def appliquer(fonction, valeurs): resultats = [] for valeur in valeurs: resultats.append(fonction(valeur)) return resultats print(appliquer(carre, [1, 2, 3])) # → [1, 4, 9] print(appliquer(cube, [1, 2, 3])) # → [1, 8, 27] ``` Tu as déjà utilisé ce principe sans le savoir avec `sorted(mots, key=len)` : on passe la fonction `len` (sans parenthèses) à `sorted`, qui l'appelle elle-même sur chaque mot. On ira plus loin avec les fonctions `lambda` au [chapitre 06](06-chaines-et-comprehensions.md). --- ### 10. La récursivité : une fonction qui s'appelle elle-même Une fonction **récursive** est une fonction qui s'appelle elle-même. Cela paraît étrange, mais c'est très naturel pour certains problèmes. **Analogie :** les poupées russes. Pour ouvrir toutes les poupées, tu ouvres la première… et à l'intérieur, tu as le même problème, en plus petit. Tu t'arrêtes quand la poupée ne s'ouvre plus : c'est le **cas de base**. Exemple : la factorielle. `5! = 5 × 4 × 3 × 2 × 1`. On remarque que `5! = 5 × 4!`, et `4! = 4 × 3!`, etc. Et par définition `1! = 1` (et `0! = 1`). ```python def factorielle(n): if n <= 1: # cas de base : on s'arrête return 1 return n * factorielle(n - 1) # appel récursif sur un problème plus petit print(factorielle(5)) # → 120 print(factorielle(10)) # → 3628800 ``` Voici ce qui se passe pour `factorielle(4)` : ```text factorielle(4) = 4 * factorielle(3) factorielle(3) = 3 * factorielle(2) factorielle(2) = 2 * factorielle(1) factorielle(1) = 1 factorielle(2) = 2 * 1 = 2 factorielle(3) = 3 * 2 = 6 factorielle(4) = 4 * 6 = 24 ``` Une fonction récursive a **toujours** deux ingrédients : 1. un **cas de base** qui renvoie un résultat sans s'appeler (sinon elle tourne à l'infini) ; 2. un **appel récursif** sur un problème **plus petit**, qui se rapproche du cas de base. Autre exemple : un compte à rebours. ```python def compte_a_rebours(n): if n == 0: print("Décollage !") return print(n) compte_a_rebours(n - 1) compte_a_rebours(3) ``` ```text 3 2 1 Décollage ! ``` La récursivité est idéale pour les structures « emboîtées » : des listes de listes de listes, des dossiers contenant des sous-dossiers… Exemple : additionner tous les nombres d'une liste imbriquée à n'importe quelle profondeur. ```python def somme_imbriquee(elements): total = 0 for element in elements: if type(element) == list: total += somme_imbriquee(element) # une sous-liste : même problème, plus petit else: total += element return total print(somme_imbriquee([1, [2, 3], [4, [5, 6]], 7])) # → 28 ``` Si on oublie le cas de base, Python finit par abandonner : ```python def infini(n): return infini(n + 1) infini(0) ``` ```text Traceback (most recent call last): File "", line 5, in File "", line 2, in infini File "", line 2, in infini File "", line 2, in infini [Previous line repeated 996 more times] RecursionError: maximum recursion depth exceeded ``` Python limite la profondeur à environ 1000 appels. Pour la plupart des problèmes simples (somme, factorielle sur de grands nombres…), une boucle reste préférable ; garde la récursivité pour les structures naturellement emboîtées. --- ### 11. Structurer un programme avec des fonctions Quand ton programme grossit, découpe-le en fonctions qui ont **chacune une seule responsabilité**, et regroupe le programme principal dans une fonction `main()`. ```python def demander_nombre(message): """Demande un entier positif à l'utilisateur jusqu'à obtenir une saisie valide.""" saisie = input(message) while not saisie.isdigit(): print("Merci d'entrer un nombre entier positif.") saisie = input(message) return int(saisie) def calculer_prix(prix_unitaire, quantite, remise=0): """Renvoie le prix total après application d'une remise en pourcentage.""" total = prix_unitaire * quantite return total * (1 - remise / 100) def afficher_ticket(quantite, total): """Affiche un petit ticket de caisse.""" print("-" * 20) print(f"Quantité : {quantite}") print(f"Total : {total:.2f} €") print("-" * 20) def main(): quantite = demander_nombre("Combien de places ? ") remise = 10 if quantite >= 5 else 0 total = calculer_prix(12.50, quantite, remise) afficher_ticket(quantite, total) main() ``` Avec les saisies `trois` puis `6` : ```text Combien de places ? trois Merci d'entrer un nombre entier positif. Combien de places ? 6 -------------------- Quantité : 6 Total : 67.50 € -------------------- ``` > `remise = 10 if quantite >= 5 else 0` est une **expression conditionnelle** (on dit aussi « ternaire ») : la valeur est `10` si la condition est vraie, `0` sinon. C'est un raccourci pour un `if/else` qui ne sert qu'à choisir une valeur. Avantages de ce découpage : - `main()` se lit comme un résumé du programme ; - chaque fonction est courte et facile à vérifier ; - `calculer_prix` ne fait ni `input` ni `print` : elle est réutilisable partout et facile à tester. > Au [chapitre 09](09-modules-bibliotheque-standard.md), tu verras qu'on remplace souvent le simple appel `main()` par `if __name__ == "__main__": main()`. --- ## Pièges fréquents ### Piège 1 — Oublier le `return` ```python def calculer_moyenne(notes): moyenne = sum(notes) / len(notes) resultat = calculer_moyenne([10, 14]) print(resultat) # → None ``` La fonction calcule bien la moyenne… puis la jette. Sans `return`, elle renvoie `None`. Et ça casse souvent plus loin : ```python def calculer_moyenne(notes): moyenne = sum(notes) / len(notes) resultat = calculer_moyenne([10, 14]) print(resultat + 1) ``` ```text Traceback (most recent call last): File "", line 6, in TypeError: unsupported operand type(s) for +: 'NoneType' and 'int' ``` **Correction :** ajouter `return moyenne`. Quand tu vois `NoneType` dans une erreur, pense tout de suite à un `return` oublié. ### Piège 2 — Utiliser `print` au lieu de `return` ```python def ajouter_tva(prix): print(prix * 1.2) total = ajouter_tva(10) + ajouter_tva(20) ``` ```text 12.0 24.0 Traceback (most recent call last): File "", line 5, in TypeError: unsupported operand type(s) for +: 'NoneType' and 'NoneType' ``` **Correction :** `return prix * 1.2`, et faire le `print` à l'extérieur, là où on a besoin d'afficher. ### Piège 3 — L'argument par défaut mutable C'est un piège célèbre, même chez les développeurs expérimentés : ```python def ajouter_article(article, panier=[]): panier.append(article) return panier print(ajouter_article("pain")) # → ['pain'] print(ajouter_article("lait")) # → ['pain', 'lait'] ``` On s'attendait à `['lait']` ! La valeur par défaut `[]` est créée **une seule fois**, quand Python lit la ligne `def`. Tous les appels partagent donc **la même liste**, qui grossit d'appel en appel. **Correction :** utiliser `None` comme valeur par défaut, et créer la liste dans la fonction. ```python def ajouter_article(article, panier=None): if panier is None: panier = [] panier.append(article) return panier print(ajouter_article("pain")) # → ['pain'] print(ajouter_article("lait")) # → ['lait'] ``` Règle : **jamais de liste, de dictionnaire ou d'ensemble comme valeur par défaut**. ### Piège 4 — Oublier les parenthèses à l'appel ```python def dire_bonjour(): return "Bonjour" message = dire_bonjour print(message) ``` Sortie (l'adresse varie à chaque exécution) : ```text ``` Sans parenthèses, on ne **appelle** pas la fonction : on manipule la fonction elle-même. **Correction :** `message = dire_bonjour()`. ### Piège 5 — Code placé après `return` ```python def diviser(a, b): return a / b print("Division effectuée") # jamais exécuté ! print(diviser(10, 4)) # → 2.5 ``` Tout ce qui suit un `return` (dans le même bloc) n'est jamais exécuté. **Correction :** placer le `print` avant le `return`, ou le supprimer. ### Piège 6 — Modifier une variable globale sans le savoir… ou sans le pouvoir Le code de la section 7 avec `compteur += 1` dans une fonction provoque `UnboundLocalError`. **Correction :** passer la valeur en paramètre et la renvoyer (`compteur = incrementer(compteur)`) plutôt que d'utiliser `global`. ### Piège 7 — Un `return` mal indenté dans une boucle ```python def contient_negatif(nombres): for nombre in nombres: if nombre < 0: return True return False # MAUVAISE indentation : dans la boucle ! print(contient_negatif([3, 5, -2])) # → False ``` La fonction renvoie `False` dès le premier élément positif, sans regarder les autres. **Correction :** le `return False` doit être **après** la boucle (au même niveau que le `for`), pour n'être atteint que si aucun négatif n'a été trouvé. ```python def contient_negatif(nombres): for nombre in nombres: if nombre < 0: return True return False print(contient_negatif([3, 5, -2])) # → True ``` --- ## Exercices ### Exercice 1 — Fonctions simples ⭐ Écris : - `aire_rectangle(longueur, largeur)` qui renvoie l'aire ; - `est_pair(n)` qui renvoie `True` si `n` est pair, `False` sinon ; - `plus_grand(a, b, c)` qui renvoie le plus grand des trois nombres **sans utiliser `max`**. Puis teste-les : ```python print(aire_rectangle(4, 2.5)) # 10.0 print(est_pair(7)) # False print(est_pair(10)) # True print(plus_grand(3, 9, 5)) # 9 ``` ### Exercice 2 — Convertisseur avec valeur par défaut ⭐ Écris une fonction `convertir(montant, taux=1.08, symbole="$")` qui renvoie une chaîne affichant le montant converti avec 2 décimales et le symbole. ```python print(convertir(100)) # 108.00 $ print(convertir(50, 0.85, "£")) # 42.50 £ print(convertir(1000, symbole="CHF", taux=0.94)) # 940.00 CHF ``` ### Exercice 3 — Statistiques sur une liste ⭐⭐ Écris une fonction `statistiques(nombres)` qui renvoie un **dictionnaire** avec les clés `"min"`, `"max"`, `"moyenne"` (arrondie à 2 décimales avec `round(valeur, 2)`) et `"mediane"`. La médiane est la valeur centrale de la liste triée ; si la liste a un nombre pair d'éléments, c'est la moyenne des deux valeurs centrales. La liste d'origine ne doit **pas** être modifiée. ```python notes = [12, 5, 18, 9, 14, 11] print(statistiques(notes)) # {'min': 5, 'max': 18, 'moyenne': 11.5, 'mediane': 11.5} print(statistiques([3, 1, 2])) # {'min': 1, 'max': 3, 'moyenne': 2.0, 'mediane': 2} print(notes) # [12, 5, 18, 9, 14, 11] ``` ### Exercice 4 — Validateur de mot de passe ⭐⭐ Écris une fonction `verifier_mot_de_passe(mdp)` qui renvoie une **liste de problèmes** (liste vide si tout va bien). Règles : au moins 8 caractères, au moins un chiffre, au moins une majuscule. Les méthodes `caractere.isdigit()` et `caractere.isupper()` te seront utiles. Écris ensuite une fonction `afficher_verdict(mdp)` qui utilise la première pour afficher le résultat. ```python afficher_verdict("abc") afficher_verdict("motdepasse1") afficher_verdict("MotDePasse2024") ``` ```text 'abc' refusé : - moins de 8 caractères - aucun chiffre - aucune majuscule 'motdepasse1' refusé : - aucune majuscule 'MotDePasse2024' accepté. ``` ### Exercice 5 — Somme flexible ⭐⭐ Écris une fonction `somme_ponderee(*valeurs, **options)` qui additionne toutes les valeurs. L'option `coef` (défaut 1) multiplie le total, et l'option `arrondi` (défaut 2) indique le nombre de décimales du résultat (avec `round`). ```python print(somme_ponderee(1, 2, 3)) # 6 print(somme_ponderee(1.5, 2.25, coef=2)) # 7.5 print(somme_ponderee(10, 3.333, coef=1.5, arrondi=1)) # 20.0 print(somme_ponderee()) # 0 ``` ### Exercice 6 — Puissance et palindrome récursifs ⭐⭐ Écris deux fonctions **récursives** (sans boucle) : - `puissance(base, exposant)` qui calcule `base ** exposant` pour un exposant entier positif ou nul, en utilisant `base ** n = base × base ** (n-1)` et `base ** 0 = 1` ; - `est_palindrome(mot)` qui renvoie `True` si le mot se lit pareil dans les deux sens. Indice : un mot de 0 ou 1 lettre est un palindrome ; sinon, il faut que la première et la dernière lettre soient égales **et** que `mot[1:-1]` (le mot sans ses extrémités) soit un palindrome. ```python print(puissance(2, 10)) # 1024 print(puissance(5, 0)) # 1 print(est_palindrome("radar")) # True print(est_palindrome("python")) # False print(est_palindrome("ressasser")) # True ``` ### Exercice 7 — Gestion de bibliothèque ⭐⭐⭐ On représente une bibliothèque par une liste de dictionnaires : ```python livres = [ {"titre": "Le Petit Prince", "auteur": "Saint-Exupéry", "annee": 1943, "dispo": True}, {"titre": "1984", "auteur": "Orwell", "annee": 1949, "dispo": False}, {"titre": "Vol de nuit", "auteur": "Saint-Exupéry", "annee": 1931, "dispo": True}, {"titre": "La Ferme des animaux", "auteur": "Orwell", "annee": 1945, "dispo": True}, ] ``` Écris les fonctions suivantes (aucune ne doit utiliser `print`, sauf la dernière) : - `livres_de(livres, auteur)` : renvoie la liste des **titres** de cet auteur ; - `livres_disponibles(livres)` : renvoie la liste des titres disponibles ; - `emprunter(livres, titre)` : si le livre existe et est disponible, le marque indisponible et renvoie `True` ; sinon renvoie `False` ; - `plus_ancien(livres)` : renvoie le dictionnaire du livre le plus ancien ; - `afficher(livres)` : affiche chaque livre au format `[x] Titre (Auteur, année)` ou `[ ]` s'il est emprunté. ```python print(livres_de(livres, "Orwell")) print(emprunter(livres, "Vol de nuit")) print(emprunter(livres, "Vol de nuit")) print(emprunter(livres, "Dune")) print(livres_disponibles(livres)) print(plus_ancien(livres)["titre"]) afficher(livres) ``` ```text ['1984', 'La Ferme des animaux'] True False False ['Le Petit Prince', 'La Ferme des animaux'] Vol de nuit [x] Le Petit Prince (Saint-Exupéry, 1943) [ ] 1984 (Orwell, 1949) [ ] Vol de nuit (Saint-Exupéry, 1931) [x] La Ferme des animaux (Orwell, 1945) ``` ### Exercice 8 — Aplatir une liste ⭐⭐⭐ Écris une fonction récursive `aplatir(elements)` qui transforme une liste imbriquée à n'importe quelle profondeur en une liste « plate », en conservant l'ordre. ```python print(aplatir([1, [2, 3], [4, [5, [6, 7]]], 8])) # [1, 2, 3, 4, 5, 6, 7, 8] print(aplatir([[["a"]], "b", []])) # ['a', 'b'] ``` --- ## Mini-projet — Le jeu du pendu, structuré en fonctions ### Cahier des charges Programme le célèbre jeu du pendu : - l'ordinateur choisit un mot secret dans une liste ; - le joueur propose une lettre à chaque tour ; - on affiche le mot avec les lettres trouvées et des `_` pour les autres (ex. `P _ T H _ N`) ; - le joueur a droit à **6 erreurs** ; - une lettre déjà proposée n'est pas comptée comme une erreur, mais on prévient le joueur ; - une saisie invalide (vide, plusieurs caractères, pas une lettre) est refusée ; - la partie se termine par une victoire ou une défaite, en révélant le mot. **Contrainte principale :** le programme doit être découpé en fonctions courtes, chacune avec une docstring. Le programme principal se résume à un appel à `main()`. Pour choisir un mot au hasard, on utilise le module `random` (présenté en détail au [chapitre 09](09-modules-bibliotheque-standard.md)) : ```python import random mot = random.choice(["python", "fonction", "variable"]) print(mot in ["python", "fonction", "variable"]) # → True ``` ### Étapes suggérées 1. `choisir_mot(mots)` : renvoie un mot au hasard, en majuscules. 2. `masquer(mot, lettres_trouvees)` : renvoie la chaîne `P _ T H _ N` (indice : construis une liste de caractères puis assemble-la avec `" ".join(liste)`, qu'on verra en détail au chapitre 06). 3. `demander_lettre(deja_proposees)` : boucle jusqu'à obtenir une lettre valide (`isalpha()` et longueur 1) et pas déjà proposée ; renvoie la lettre en majuscule. 4. `mot_complet(mot, lettres_trouvees)` : renvoie `True` si toutes les lettres du mot ont été trouvées. 5. `afficher_etat(...)` : affiche le mot masqué, les erreurs restantes et les lettres déjà proposées. 6. `jouer_partie(mot)` : la boucle de jeu ; renvoie `True` en cas de victoire. 7. `main()` : choisit le mot, lance la partie, affiche le message final. Astuce de test : pour tester ton programme sans hasard, appelle directement `jouer_partie("PYTHON")`. ### Pistes d'amélioration - Dessiner le pendu en ASCII art (une liste de 7 dessins, indexée par le nombre d'erreurs). - Proposer de rejouer à la fin et tenir un score (victoires / défaites). - Gérer les accents : considérer que `E` trouve aussi `É`, `È`, `Ê`. - Choisir un niveau de difficulté (longueur du mot, nombre d'erreurs autorisées). - Lire la liste des mots depuis un fichier texte ([chapitre 07](07-fichiers-csv-json.md)). --- ## Corrigés
Corrigé de l'exercice 1 ```python def aire_rectangle(longueur, largeur): """Renvoie l'aire d'un rectangle.""" return longueur * largeur def est_pair(n): """Renvoie True si n est pair.""" return n % 2 == 0 def plus_grand(a, b, c): """Renvoie le plus grand des trois nombres.""" maximum = a if b > maximum: maximum = b if c > maximum: maximum = c return maximum print(aire_rectangle(4, 2.5)) print(est_pair(7)) print(est_pair(10)) print(plus_grand(3, 9, 5)) ``` Sortie : ```text 10.0 False True 9 ``` `est_pair` renvoie directement le résultat de la comparaison `n % 2 == 0`, qui est déjà un booléen : inutile d'écrire `if ...: return True else: return False`. Pour `plus_grand`, on part du principe que `a` est le plus grand et on corrige si on trouve mieux.
Corrigé de l'exercice 2 ```python def convertir(montant, taux=1.08, symbole="$"): """Convertit un montant en euros dans une autre devise et renvoie une chaîne formatée.""" converti = montant * taux return f"{converti:.2f} {symbole}" print(convertir(100)) print(convertir(50, 0.85, "£")) print(convertir(1000, symbole="CHF", taux=0.94)) ``` Sortie : ```text 108.00 $ 42.50 £ 940.00 CHF ``` Le troisième appel montre l'intérêt des arguments nommés : l'ordre n'importe plus. La fonction **renvoie** la chaîne au lieu de l'afficher : on pourrait ainsi l'écrire dans un fichier ou la combiner avec d'autres textes.
Corrigé de l'exercice 3 ```python def mediane(nombres): """Renvoie la médiane d'une liste non vide de nombres.""" tries = sorted(nombres) # copie triée : l'original n'est pas touché n = len(tries) milieu = n // 2 if n % 2 == 1: return tries[milieu] return (tries[milieu - 1] + tries[milieu]) / 2 def statistiques(nombres): """Renvoie un dictionnaire avec le min, le max, la moyenne et la médiane.""" return { "min": min(nombres), "max": max(nombres), "moyenne": round(sum(nombres) / len(nombres), 2), "mediane": mediane(nombres), } notes = [12, 5, 18, 9, 14, 11] print(statistiques(notes)) print(statistiques([3, 1, 2])) print(notes) ``` Sortie : ```text {'min': 5, 'max': 18, 'moyenne': 11.5, 'mediane': 11.5} {'min': 1, 'max': 3, 'moyenne': 2.0, 'mediane': 2} [12, 5, 18, 9, 14, 11] ``` On a extrait le calcul de la médiane dans sa propre fonction : chaque fonction fait une seule chose. `sorted()` (et non `.sort()`) garantit que la liste de l'appelant n'est pas modifiée. Pour 6 éléments triés `[5, 9, 11, 12, 14, 18]`, `milieu` vaut 3 : on fait la moyenne de `tries[2]` (11) et `tries[3]` (12).
Corrigé de l'exercice 4 ```python def verifier_mot_de_passe(mdp): """Renvoie la liste des règles non respectées par mdp (vide si tout va bien).""" problemes = [] if len(mdp) < 8: problemes.append("moins de 8 caractères") a_chiffre = False a_majuscule = False for caractere in mdp: if caractere.isdigit(): a_chiffre = True if caractere.isupper(): a_majuscule = True if not a_chiffre: problemes.append("aucun chiffre") if not a_majuscule: problemes.append("aucune majuscule") return problemes def afficher_verdict(mdp): """Affiche si le mot de passe est accepté, et sinon pourquoi.""" problemes = verifier_mot_de_passe(mdp) if len(problemes) == 0: print(f"'{mdp}' accepté.") else: print(f"'{mdp}' refusé :") for probleme in problemes: print(f" - {probleme}") afficher_verdict("abc") afficher_verdict("motdepasse1") afficher_verdict("MotDePasse2024") ``` Sortie : ```text 'abc' refusé : - moins de 8 caractères - aucun chiffre - aucune majuscule 'motdepasse1' refusé : - aucune majuscule 'MotDePasse2024' accepté. ``` La séparation est importante : `verifier_mot_de_passe` **calcule** (et pourrait servir dans une application web), `afficher_verdict` **présente**. Les booléens `a_chiffre` et `a_majuscule` sont des « drapeaux » qui passent à `True` dès qu'on trouve un caractère correspondant.
Corrigé de l'exercice 5 ```python def somme_ponderee(*valeurs, **options): """Additionne les valeurs, multiplie par l'option coef et arrondit.""" coef = options.get("coef", 1) arrondi = options.get("arrondi", 2) total = sum(valeurs) * coef return round(total, arrondi) print(somme_ponderee(1, 2, 3)) print(somme_ponderee(1.5, 2.25, coef=2)) print(somme_ponderee(10, 3.333, coef=1.5, arrondi=1)) print(somme_ponderee()) ``` Sortie : ```text 6 7.5 20.0 0 ``` `valeurs` est un tuple (on peut l'envoyer directement à `sum`), `options` est un dictionnaire : `get` avec une valeur par défaut est donc parfait. Note que `round` appliqué à un entier renvoie un entier (`6`), et appliqué à un float renvoie un float. Dans un vrai programme, on écrirait plutôt `def somme_ponderee(*valeurs, coef=1, arrondi=2)` : des paramètres nommés explicites sont plus clairs que `**options`, mais l'exercice sert à manipuler `**kwargs`.
Corrigé de l'exercice 6 ```python def puissance(base, exposant): """Calcule base ** exposant récursivement (exposant entier >= 0).""" if exposant == 0: # cas de base return 1 return base * puissance(base, exposant - 1) # problème plus petit def est_palindrome(mot): """Renvoie True si mot se lit de la même façon dans les deux sens.""" if len(mot) <= 1: # cas de base return True if mot[0] != mot[-1]: # extrémités différentes : perdu return False return est_palindrome(mot[1:-1]) # on retire les extrémités et on recommence print(puissance(2, 10)) print(puissance(5, 0)) print(est_palindrome("radar")) print(est_palindrome("python")) print(est_palindrome("ressasser")) ``` Sortie : ```text 1024 1 True False True ``` Pour `"radar"` : `r == r`, on teste `"ada"` ; `a == a`, on teste `"d"` ; une seule lettre, c'est le cas de base, `True`. Chaque appel travaille sur un mot plus court de 2 lettres : on se rapproche forcément du cas de base.
Corrigé de l'exercice 7 ```python def livres_de(livres, auteur): """Renvoie la liste des titres écrits par auteur.""" titres = [] for livre in livres: if livre["auteur"] == auteur: titres.append(livre["titre"]) return titres def livres_disponibles(livres): """Renvoie la liste des titres disponibles.""" titres = [] for livre in livres: if livre["dispo"]: titres.append(livre["titre"]) return titres def emprunter(livres, titre): """Marque le livre comme emprunté. Renvoie True si l'emprunt a réussi.""" for livre in livres: if livre["titre"] == titre: if livre["dispo"]: livre["dispo"] = False return True return False # trouvé mais déjà emprunté return False # pas trouvé du tout def plus_ancien(livres): """Renvoie le dictionnaire du livre le plus ancien.""" ancien = livres[0] for livre in livres: if livre["annee"] < ancien["annee"]: ancien = livre return ancien def afficher(livres): """Affiche la liste des livres avec leur disponibilité.""" for livre in livres: case = "x" if livre["dispo"] else " " print(f"[{case}] {livre['titre']} ({livre['auteur']}, {livre['annee']})") livres = [ {"titre": "Le Petit Prince", "auteur": "Saint-Exupéry", "annee": 1943, "dispo": True}, {"titre": "1984", "auteur": "Orwell", "annee": 1949, "dispo": False}, {"titre": "Vol de nuit", "auteur": "Saint-Exupéry", "annee": 1931, "dispo": True}, {"titre": "La Ferme des animaux", "auteur": "Orwell", "annee": 1945, "dispo": True}, ] print(livres_de(livres, "Orwell")) print(emprunter(livres, "Vol de nuit")) print(emprunter(livres, "Vol de nuit")) print(emprunter(livres, "Dune")) print(livres_disponibles(livres)) print(plus_ancien(livres)["titre"]) afficher(livres) ``` Sortie : ```text ['1984', 'La Ferme des animaux'] True False False ['Le Petit Prince', 'La Ferme des animaux'] Vol de nuit [x] Le Petit Prince (Saint-Exupéry, 1943) [ ] 1984 (Orwell, 1949) [ ] Vol de nuit (Saint-Exupéry, 1931) [x] La Ferme des animaux (Orwell, 1945) ``` `emprunter` **modifie** le dictionnaire reçu : comme la liste est passée par référence, la modification est visible après l'appel (c'est voulu ici). Note les deux `return False` : l'un quand le livre est trouvé mais indisponible, l'autre après la boucle quand il n'a pas été trouvé. On pourrait renvoyer des messages différents pour distinguer les deux cas.
Corrigé de l'exercice 8 ```python def aplatir(elements): """Transforme une liste imbriquée en liste à un seul niveau.""" resultat = [] for element in elements: if type(element) == list: resultat.extend(aplatir(element)) # on aplatit la sous-liste, puis on ajoute ses éléments else: resultat.append(element) return resultat print(aplatir([1, [2, 3], [4, [5, [6, 7]]], 8])) print(aplatir([[["a"]], "b", []])) ``` Sortie : ```text [1, 2, 3, 4, 5, 6, 7, 8] ['a', 'b'] ``` Le cas de base est implicite : un élément qui n'est pas une liste est simplement ajouté, et une liste vide ne déclenche aucun tour de boucle. Note l'usage d'`extend` (ajouter **les éléments** de la liste aplatie) et non d'`append` (qui ajouterait la liste entière, cf. piège 7 du chapitre 04).
Corrigé du mini-projet ```python import random MOTS = ["python", "fonction", "variable", "boucle", "dictionnaire", "liste"] ERREURS_MAX = 6 def choisir_mot(mots): """Renvoie un mot choisi au hasard, en majuscules.""" return random.choice(mots).upper() def masquer(mot, lettres_trouvees): """Renvoie le mot avec des _ à la place des lettres non trouvées.""" affichage = [] for lettre in mot: if lettre in lettres_trouvees: affichage.append(lettre) else: affichage.append("_") return " ".join(affichage) def mot_complet(mot, lettres_trouvees): """Renvoie True si toutes les lettres du mot ont été trouvées.""" for lettre in mot: if lettre not in lettres_trouvees: return False return True def demander_lettre(deja_proposees): """Demande une lettre valide et pas encore proposée ; la renvoie en majuscule.""" while True: saisie = input("Ta lettre : ").strip().upper() if len(saisie) != 1 or not saisie.isalpha(): print("Tape une seule lettre.") elif saisie in deja_proposees: print(f"Tu as déjà proposé {saisie}.") else: return saisie def afficher_etat(mot, lettres_trouvees, erreurs, deja_proposees): """Affiche le mot masqué, les erreurs restantes et les lettres déjà jouées.""" print() print(masquer(mot, lettres_trouvees)) print(f"Erreurs restantes : {ERREURS_MAX - erreurs}") if deja_proposees: print(f"Déjà proposées : {' '.join(sorted(deja_proposees))}") def jouer_partie(mot): """Déroule une partie complète. Renvoie True si le joueur gagne.""" lettres_trouvees = set() deja_proposees = set() erreurs = 0 while erreurs < ERREURS_MAX: afficher_etat(mot, lettres_trouvees, erreurs, deja_proposees) lettre = demander_lettre(deja_proposees) deja_proposees.add(lettre) if lettre in mot: lettres_trouvees.add(lettre) print(f"Bien joué, {lettre} est dans le mot !") if mot_complet(mot, lettres_trouvees): return True else: erreurs += 1 print(f"Raté, pas de {lettre}.") return False def main(): """Point d'entrée du jeu.""" print("=== JEU DU PENDU ===") mot = choisir_mot(MOTS) if jouer_partie(mot): print(f"\nBravo ! Tu as trouvé le mot {mot}.") else: print(f"\nPerdu... Le mot était {mot}.") main() ``` Pour vérifier le déroulement sans hasard, voici une partie avec le mot fixé (on remplace `main()` par l'appel direct ci-dessous) et les saisies `e`, `p`, `ab`, `y`, `p`, `t`, `h`, `o`, `n` : ```python # (à placer après les définitions précédentes, à la place de main()) if jouer_partie("PYTHON"): print("\nBravo !") ``` ```text _ _ _ _ _ _ Erreurs restantes : 6 Ta lettre : e Raté, pas de E. _ _ _ _ _ _ Erreurs restantes : 5 Déjà proposées : E Ta lettre : p Bien joué, P est dans le mot ! P _ _ _ _ _ Erreurs restantes : 5 Déjà proposées : E P Ta lettre : ab Tape une seule lettre. Ta lettre : y Bien joué, Y est dans le mot ! P Y _ _ _ _ Erreurs restantes : 5 Déjà proposées : E P Y Ta lettre : p Tu as déjà proposé P. Ta lettre : t Bien joué, T est dans le mot ! P Y T _ _ _ Erreurs restantes : 5 Déjà proposées : E P T Y Ta lettre : h Bien joué, H est dans le mot ! P Y T H _ _ Erreurs restantes : 5 Déjà proposées : E H P T Y Ta lettre : o Bien joué, O est dans le mot ! P Y T H O _ Erreurs restantes : 5 Déjà proposées : E H O P T Y Ta lettre : n Bien joué, N est dans le mot ! Bravo ! ``` **Points clés de la solution :** - Chaque fonction a **une seule responsabilité** et une docstring ; `main()` se lit comme le scénario du jeu. - `masquer` et `mot_complet` ne font aucun `print` ni `input` : ce sont des fonctions « pures », faciles à tester (`masquer("PYTHON", {"P", "N"})` doit renvoyer `"P _ _ _ _ N"`). - Les lettres sont stockées dans des **ensembles** (chapitre 04) : pas de doublon, test `in` rapide. - `demander_lettre` utilise `while True` + `return` : la fonction ne rend la main qu'avec une saisie valide. - Les constantes `MOTS` et `ERREURS_MAX` sont en majuscules et en haut du fichier : pour changer la difficulté, une seule ligne à modifier. - Pouvoir appeler `jouer_partie("PYTHON")` directement montre l'avantage d'avoir séparé le choix du mot du déroulement de la partie.
--- ## Récapitulatif - Une fonction se **définit** avec `def nom(parametres):` et s'**appelle** avec `nom(arguments)`. Elle doit être définie avant d'être appelée. - `return` **renvoie** une valeur au code appelant et **arrête** la fonction. Sans `return`, une fonction renvoie `None`. - `print` montre à l'humain ; `return` donne au programme. Une fonction de calcul doit **renvoyer** son résultat. - On peut renvoyer plusieurs valeurs (un tuple) et les déballer : `a, b = f()`. - Les **valeurs par défaut** (`def f(x, y=0)`) rendent des paramètres optionnels ; les **arguments nommés** (`f(x=1, y=2)`) rendent les appels lisibles. - `*args` regroupe les arguments positionnels en trop dans un **tuple**, `**kwargs` les arguments nommés en trop dans un **dictionnaire**. - Les variables créées dans une fonction sont **locales**. Évite `global` : passe les données en paramètres et renvoie les résultats. - Une liste passée en paramètre peut être modifiée par la fonction. **Jamais** de valeur par défaut mutable : utilise `None`. - Une **docstring** documente la fonction juste sous le `def`. - Une fonction **récursive** s'appelle elle-même ; elle a besoin d'un **cas de base** et d'un appel sur un problème plus petit. ### Checklist de validation - [ ] Je sais écrire une fonction avec paramètres qui renvoie un résultat. - [ ] Je sais expliquer la différence entre `print` et `return`. - [ ] Je sais utiliser une valeur par défaut et appeler une fonction avec des arguments nommés. - [ ] Je connais le piège de l'argument par défaut mutable et sa correction. - [ ] Je sais pourquoi une variable créée dans une fonction n'existe pas à l'extérieur. - [ ] J'écris une docstring pour chacune de mes fonctions. - [ ] Je sais écrire une fonction récursive avec un cas de base. - [ ] J'ai terminé le pendu en le découpant en fonctions. --- [← Chapitre précédent](04-structures-de-donnees.md) | [Sommaire](README.md) | [Chapitre suivant →](06-chaines-et-comprehensions.md)