alpinux-wiki/docs/presentations/Python/05-fonctions.md
2026-10-15 00:28:56 +02:00

1630 lines
50 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 "<stdin>", line 1, in <module>
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 "<stdin>", line 5, in <module>
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 "<stdin>", line 6, in <module>
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 "<stdin>", line 8, in <module>
File "<stdin>", 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 "<stdin>", line 5, in <module>
File "<stdin>", line 2, in infini
File "<stdin>", line 2, in infini
File "<stdin>", 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 "<stdin>", line 6, in <module>
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 "<stdin>", line 5, in <module>
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
<function dire_bonjour at 0x7f3a2c1b8e00>
```
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
<details>
<summary>Corrigé de l'exercice 1</summary>
```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.
</details>
<details>
<summary>Corrigé de l'exercice 2</summary>
```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.
</details>
<details>
<summary>Corrigé de l'exercice 3</summary>
```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).
</details>
<details>
<summary>Corrigé de l'exercice 4</summary>
```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.
</details>
<details>
<summary>Corrigé de l'exercice 5</summary>
```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`.
</details>
<details>
<summary>Corrigé de l'exercice 6</summary>
```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.
</details>
<details>
<summary>Corrigé de l'exercice 7</summary>
```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.
</details>
<details>
<summary>Corrigé de l'exercice 8</summary>
```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).
</details>
<details>
<summary>Corrigé du mini-projet</summary>
```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.
</details>
---
## 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)