1630 lines
50 KiB
Markdown
1630 lines
50 KiB
Markdown
# 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)
|