alpinux-wiki/docs/presentations/Python/04-structures-de-donnees.md
2026-10-15 00:28:56 +02:00

1717 lines
50 KiB
Markdown

# Chapitre 04 — Les structures de données : listes, tuples, dictionnaires, ensembles
> **Durée estimée :** 5 à 6 heures (cours, exercices et mini-projet)
>
> **Prérequis :** [Chapitre 03 — Les boucles](03-boucles.md) (`for`, `while`, `range`)
>
> **Objectifs — à la fin de ce chapitre, tu sauras…**
>
> - créer, lire, modifier et parcourir une **liste** ;
> - comprendre la différence entre **copier** une liste et lui donner un **deuxième nom** ;
> - utiliser un **tuple** et « déballer » plusieurs valeurs d'un coup ;
> - ranger des informations par nom grâce aux **dictionnaires** ;
> - éliminer des doublons et comparer des collections avec les **ensembles** ;
> - choisir la structure adaptée à chaque problème.
---
## Cours
### 1. Pourquoi des structures de données ?
Jusqu'ici, chaque variable contenait **une seule** valeur : un nombre, un texte, un booléen. Imagine que tu veuilles retenir les notes de toute une classe :
```python
note1 = 12
note2 = 15
note3 = 9
note4 = 17
# ... et s'il y a 30 élèves ? 300 ?
```
C'est vite ingérable : impossible de faire une boucle sur `note1`, `note2`, `note3`… Il nous faut une variable capable de contenir **plusieurs valeurs** à la fois. C'est le rôle des **structures de données** (on dit aussi **collections**).
Python en propose quatre, que tu utiliseras en permanence :
| Structure | Exemple | Analogie |
|---|---|---|
| liste (`list`) | `[12, 15, 9]` | une étagère numérotée où l'on peut ajouter, retirer, réordonner |
| tuple (`tuple`) | `(48.85, 2.35)` | une fiche plastifiée : on la lit, on ne la modifie pas |
| dictionnaire (`dict`) | `{"Alice": 12, "Bob": 15}` | un répertoire : on cherche par **nom**, pas par numéro |
| ensemble (`set`) | `{"rouge", "vert"}` | un sac de billes toutes différentes, sans ordre |
---
### 2. Les listes : créer et lire
Une **liste** est une suite ordonnée d'éléments, écrite entre crochets `[ ]`, les éléments étant séparés par des virgules.
```python
notes = [12, 15, 9, 17]
prenoms = ["Alice", "Bob", "Chloé"]
vide = []
melange = [42, "texte", 3.14, True] # possible, mais rarement une bonne idée
print(notes)
print(prenoms)
print(len(notes)) # len() donne le nombre d'éléments
print(vide)
```
```text
[12, 15, 9, 17]
['Alice', 'Bob', 'Chloé']
4
[]
```
> Python affiche les chaînes d'une liste avec des apostrophes `'…'` : c'est normal, c'est sa façon de te montrer qu'il s'agit de textes.
#### Les indices : chaque case a un numéro
Chaque élément a une **position**, appelée **indice**. Attention : **on commence à compter à 0** !
```text
indice : 0 1 2 3
notes : [ 12, 15, 9, 17 ]
indice : -4 -3 -2 -1
```
```python
notes = [12, 15, 9, 17]
print(notes[0]) # → 12
print(notes[2]) # → 9
print(notes[-1]) # → 17
print(notes[-2]) # → 9
```
Les **indices négatifs** comptent depuis la fin : `-1` est le dernier élément, `-2` l'avant-dernier… Très pratique quand on ne connaît pas la longueur de la liste.
Si tu demandes une case qui n'existe pas, Python se fâche :
```python
notes = [12, 15, 9, 17]
print(notes[4])
```
```text
Traceback (most recent call last):
File "<stdin>", line 2, in <module>
IndexError: list index out of range
```
Une liste de 4 éléments a des indices de `0` à `3`. Le dernier indice valide est toujours `len(liste) - 1`.
#### Modifier un élément
Une liste est **modifiable** (on dit **mutable**) : on peut remplacer un élément en l'affectant par son indice.
```python
notes = [12, 15, 9, 17]
notes[2] = 11 # on corrige la 3e note
print(notes) # → [12, 15, 11, 17]
```
---
### 3. Ajouter et retirer des éléments
Les listes viennent avec des **méthodes** : des actions qu'on appelle avec un point, `liste.methode(...)`. Voici les plus utiles pour faire grandir ou rétrécir une liste.
#### Ajouter
```python
courses = ["pain", "lait"]
courses.append("oeufs") # ajoute À LA FIN
print(courses) # → ['pain', 'lait', 'oeufs']
courses.insert(0, "café") # insère à l'indice 0 (au début)
print(courses) # → ['café', 'pain', 'lait', 'oeufs']
courses.extend(["riz", "pâtes"]) # ajoute PLUSIEURS éléments à la fin
print(courses) # → ['café', 'pain', 'lait', 'oeufs', 'riz', 'pâtes']
```
On peut aussi **concaténer** deux listes avec `+` (cela crée une **nouvelle** liste) ou répéter une liste avec `*` :
```python
a = [1, 2]
b = [3, 4]
print(a + b) # → [1, 2, 3, 4]
print(a * 3) # → [1, 2, 1, 2, 1, 2]
print(a) # → [1, 2]
```
#### Retirer
```python
courses = ["café", "pain", "lait", "oeufs", "lait"]
courses.remove("lait") # retire la PREMIÈRE occurrence de "lait"
print(courses) # → ['café', 'pain', 'oeufs', 'lait']
dernier = courses.pop() # retire ET renvoie le dernier élément
print(dernier) # → lait
print(courses) # → ['café', 'pain', 'oeufs']
premier = courses.pop(0) # retire ET renvoie l'élément d'indice 0
print(premier) # → café
del courses[0] # supprime l'élément d'indice 0 (sans le renvoyer)
print(courses) # → ['oeufs']
courses.clear() # vide complètement la liste
print(courses) # → []
```
Résumé :
| Méthode | Effet |
|---|---|
| `liste.append(x)` | ajoute `x` à la fin |
| `liste.insert(i, x)` | insère `x` à l'indice `i` |
| `liste.extend(autre)` | ajoute tous les éléments de `autre` à la fin |
| `liste.remove(x)` | retire la première occurrence de `x` (erreur si absent) |
| `liste.pop()` / `liste.pop(i)` | retire et **renvoie** le dernier élément / celui d'indice `i` |
| `del liste[i]` | supprime l'élément d'indice `i` |
| `liste.clear()` | vide la liste |
---
### 4. Rechercher dans une liste
#### Le mot-clé `in`
Pour savoir si un élément est présent, on utilise `in` (et `not in`), qui renvoie un booléen. Tu peux donc l'utiliser directement dans un `if` :
```python
fruits = ["pomme", "banane", "kiwi"]
print("kiwi" in fruits) # → True
print("mangue" in fruits) # → False
print("mangue" not in fruits) # → True
if "banane" in fruits:
print("Il reste des bananes !") # → Il reste des bananes !
```
#### `index` et `count`
```python
lettres = ["a", "b", "c", "b", "a", "b"]
print(lettres.index("c")) # → 2
print(lettres.count("b")) # → 3
print(lettres.count("z")) # → 0
```
`index()` provoque une erreur si l'élément est absent (`ValueError: 'z' is not in list`) : vérifie d'abord avec `in`.
#### Les fonctions `min`, `max`, `sum`
Ces fonctions intégrées travaillent sur une liste entière :
```python
notes = [12, 15, 9, 17, 14]
print(min(notes)) # → 9
print(max(notes)) # → 17
print(sum(notes)) # → 67
moyenne = sum(notes) / len(notes)
print(f"Moyenne : {moyenne:.2f}") # → Moyenne : 13.40
```
---
### 5. Parcourir une liste
#### Avec `for` : la façon naturelle
Tu as vu `for` au chapitre 03 avec `range`. En réalité, `for` sait parcourir **n'importe quelle collection** : à chaque tour, la variable prend la valeur de l'élément suivant.
```python
prenoms = ["Alice", "Bob", "Chloé"]
for prenom in prenoms:
print(f"Bonjour {prenom} !")
```
```text
Bonjour Alice !
Bonjour Bob !
Bonjour Chloé !
```
Lis-le comme une phrase : « pour chaque prénom dans la liste des prénoms, affiche… ». Choisis un nom au singulier pour la variable de boucle et au pluriel pour la liste : le code devient lisible.
#### Quand on a besoin de l'indice
Parfois, tu veux aussi la position. La façon simple avec ce que tu connais : `range(len(liste))`.
```python
podium = ["Alice", "Bob", "Chloé"]
for i in range(len(podium)):
print(f"{i + 1}. {podium[i]}")
```
```text
1. Alice
2. Bob
3. Chloé
```
> Python propose une fonction plus élégante pour cela, `enumerate()`. On la verra au [chapitre 06](06-chaines-et-comprehensions.md).
#### Construire une liste au fil d'une boucle
Un schéma ultra-courant : partir d'une liste vide et la remplir avec `append`.
```python
notes = [12, 7, 15, 9, 18, 10]
notes_admises = []
for note in notes:
if note >= 10:
notes_admises.append(note)
print(notes_admises) # → [12, 15, 18, 10]
```
Et pour un **accumulateur** (total, compteur), c'est le même principe qu'au chapitre 03 :
```python
temperatures = [18.5, 21.0, 19.5, 23.0]
total = 0
nb_chaudes = 0
for temperature in temperatures:
total += temperature
if temperature > 20:
nb_chaudes += 1
print(f"Moyenne : {total / len(temperatures)}") # → Moyenne : 20.5
print(f"Jours chauds : {nb_chaudes}") # → Jours chauds : 2
```
#### Remplir une liste avec `input()`
```python
achats = []
saisie = input("Article (vide pour finir) : ")
while saisie != "":
achats.append(saisie)
saisie = input("Article (vide pour finir) : ")
print(f"Tu as noté {len(achats)} article(s) : {achats}")
```
```text
Article (vide pour finir) : tomates
Article (vide pour finir) : basilic
Article (vide pour finir) :
Tu as noté 2 article(s) : ['tomates', 'basilic']
```
---
### 6. Trier et inverser
#### `sort()` : trier la liste elle-même
```python
nombres = [5, 2, 9, 1, 7]
nombres.sort()
print(nombres) # → [1, 2, 5, 7, 9]
nombres.sort(reverse=True) # ordre décroissant
print(nombres) # → [9, 7, 5, 2, 1]
```
`sort()` **modifie** la liste et ne renvoie rien (elle renvoie `None`, voir les pièges plus bas).
#### `sorted()` : obtenir une copie triée
```python
nombres = [5, 2, 9, 1, 7]
tries = sorted(nombres)
print(tries) # → [1, 2, 5, 7, 9]
print(nombres) # → [5, 2, 9, 1, 7]
```
`sorted()` laisse l'original intact et renvoie une **nouvelle** liste. Les chaînes se trient par ordre alphabétique… avec une subtilité : les majuscules passent avant les minuscules.
```python
mots = ["poire", "Banane", "abricot", "Cerise"]
print(sorted(mots)) # → ['Banane', 'Cerise', 'abricot', 'poire']
print(sorted(mots, key=len)) # → ['poire', 'Banane', 'Cerise', 'abricot']
```
Le paramètre `key=len` signifie « trie selon la longueur ». On approfondira `sorted` et `key` au [chapitre 06](06-chaines-et-comprehensions.md).
#### `reverse()` : inverser l'ordre
```python
etapes = ["départ", "milieu", "arrivée"]
etapes.reverse()
print(etapes) # → ['arrivée', 'milieu', 'départ']
```
---
### 7. Le découpage (slicing) : extraire une portion
On peut extraire un **morceau** de liste avec la syntaxe `liste[debut:fin]`. L'élément d'indice `debut` est inclus, celui d'indice `fin` est **exclu** (comme avec `range`).
```python
lettres = ["a", "b", "c", "d", "e", "f"]
print(lettres[1:4]) # → ['b', 'c', 'd']
print(lettres[:3]) # → ['a', 'b', 'c']
print(lettres[3:]) # → ['d', 'e', 'f']
print(lettres[-2:]) # → ['e', 'f']
print(lettres[::2]) # → ['a', 'c', 'e']
print(lettres[::-1]) # → ['f', 'e', 'd', 'c', 'b', 'a']
```
- Si on omet `debut`, on part du début ; si on omet `fin`, on va jusqu'au bout.
- Un troisième nombre donne le **pas** : `[::2]` prend un élément sur deux, `[::-1]` parcourt à l'envers.
Le slicing fonctionne aussi sur les chaînes de caractères ; on l'étudiera en détail au [chapitre 06](06-chaines-et-comprehensions.md).
---
### 8. Les listes de listes
Une liste peut contenir… d'autres listes. C'est idéal pour représenter un **tableau** (une grille, une matrice, un plateau de jeu).
```python
grille = [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9],
]
print(grille[0]) # → [1, 2, 3]
print(grille[1][2]) # → 6
print(len(grille)) # → 3
```
`grille[1][2]` se lit « dans la ligne d'indice 1, l'élément d'indice 2 ». Pour tout parcourir, on utilise deux boucles imbriquées (vues au chapitre 03) :
```python
grille = [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9],
]
for ligne in grille:
for valeur in ligne:
print(valeur, end=" ")
print() # retour à la ligne après chaque ligne de la grille
```
```text
1 2 3
4 5 6
7 8 9
```
Exemple concret : un petit tableau d'élèves avec leurs notes.
```python
classe = [
["Alice", 14, 16],
["Bob", 9, 12],
["Chloé", 18, 15],
]
for eleve in classe:
nom = eleve[0]
moyenne = (eleve[1] + eleve[2]) / 2
print(f"{nom} : {moyenne}")
```
```text
Alice : 15.0
Bob : 10.5
Chloé : 16.5
```
---
### 9. Références et copies : le point le plus important du chapitre
Voici un comportement qui surprend **tous** les débutants :
```python
a = [1, 2, 3]
b = a
b.append(4)
print(a) # → [1, 2, 3, 4]
print(b) # → [1, 2, 3, 4]
```
On a modifié `b`… et `a` a changé aussi ! Pourquoi ?
**Analogie :** une variable n'est pas une boîte qui *contient* la liste, c'est une **étiquette** collée sur la liste. L'instruction `b = a` ne fabrique pas une deuxième liste : elle colle une deuxième étiquette, `b`, sur **la même** liste.
```text
a ──┐
├──► [1, 2, 3, 4]
b ──┘
```
On peut le vérifier avec l'opérateur `is`, qui teste si deux noms désignent **le même objet** :
```python
a = [1, 2, 3]
b = a
print(b is a) # → True
```
#### Faire une vraie copie
Si tu veux une liste **indépendante**, il faut la copier explicitement. Trois façons équivalentes :
```python
a = [1, 2, 3]
b = a.copy() # méthode copy()
c = list(a) # construire une nouvelle liste à partir de a
d = a[:] # slicing complet
b.append(4)
print(a) # → [1, 2, 3]
print(b) # → [1, 2, 3, 4]
print(c is a) # → False
print(c == a) # → True
```
Note la différence : `==` compare le **contenu**, `is` compare l'**identité** (même objet ou non).
#### Et pour les nombres ou les chaînes ?
Avec un `int` ou une `str`, ce problème ne se pose jamais : ces types sont **immuables** (on ne peut pas les modifier sur place). `x += 1` ne modifie pas le nombre, il crée un nouveau nombre et déplace l'étiquette.
```python
x = 5
y = x
x += 1
print(x, y) # → 6 5
```
#### Copie superficielle et listes imbriquées
`copy()` ne copie que le **premier niveau**. Pour une liste de listes, les sous-listes restent partagées :
```python
grille = [[1, 2], [3, 4]]
copie = grille.copy()
copie[0].append(99)
print(grille) # → [[1, 2, 99], [3, 4]]
```
Pour copier aussi les sous-listes, on utilise `deepcopy` du module `copy` (les modules seront vus au [chapitre 09](09-modules-bibliotheque-standard.md)) :
```python
import copy
grille = [[1, 2], [3, 4]]
copie = copy.deepcopy(grille)
copie[0].append(99)
print(grille) # → [[1, 2], [3, 4]]
print(copie) # → [[1, 2, 99], [3, 4]]
```
---
### 10. Les tuples : des listes figées
Un **tuple** ressemble à une liste, mais il est **immuable** : une fois créé, on ne peut ni modifier, ni ajouter, ni retirer d'élément. On l'écrit entre parenthèses.
```python
point = (3, 7)
couleur_rgb = (255, 128, 0)
print(point[0]) # → 3
print(len(couleur_rgb)) # → 3
print(128 in couleur_rgb) # → True
```
Tenter de le modifier provoque une erreur :
```python
point = (3, 7)
point[0] = 10
```
```text
Traceback (most recent call last):
File "<stdin>", line 2, in <module>
TypeError: 'tuple' object does not support item assignment
```
**À quoi bon ?** Un tuple sert à regrouper des valeurs qui **vont ensemble** et ne doivent pas changer : des coordonnées, une date `(14, 7, 1789)`, une couleur RGB… L'immuabilité est une **garantie** : personne ne pourra modifier ces données par erreur.
#### Le tuple à un seul élément
Piège classique : `(5)` n'est pas un tuple, c'est juste le nombre 5 entre parenthèses. Il faut une **virgule** :
```python
pas_un_tuple = (5)
un_tuple = (5,)
print(type(pas_un_tuple)) # → <class 'int'>
print(type(un_tuple)) # → <class 'tuple'>
```
En fait, c'est la **virgule** qui fait le tuple, pas les parenthèses : `point = 3, 7` crée aussi un tuple.
#### Le déballage (unpacking)
On peut répartir les éléments d'un tuple (ou d'une liste) dans plusieurs variables d'un coup :
```python
point = (3, 7)
x, y = point
print(x) # → 3
print(y) # → 7
jour, mois, annee = (14, 7, 1789)
print(f"{jour}/{mois}/{annee}") # → 14/7/1789
```
Il faut autant de variables que d'éléments, sinon : `ValueError: too many values to unpack`.
L'astérisque `*` permet de récupérer « le reste » dans une liste :
```python
premier, *autres = [10, 20, 30, 40]
print(premier) # → 10
print(autres) # → [20, 30, 40]
```
Le déballage offre une astuce célèbre : **échanger deux variables** sans variable temporaire.
```python
a = 1
b = 2
a, b = b, a
print(a, b) # → 2 1
```
Et il est très pratique dans une boucle sur une liste de tuples :
```python
villes = [("Paris", 2_100_000), ("Lyon", 520_000), ("Lille", 236_000)]
for nom, habitants in villes:
print(f"{nom} compte {habitants} habitants")
```
```text
Paris compte 2100000 habitants
Lyon compte 520000 habitants
Lille compte 236000 habitants
```
> Rappel : `2_100_000` est simplement une écriture plus lisible de `2100000` ; les `_` sont ignorés par Python.
---
### 11. Les dictionnaires : chercher par clé
Une liste range ses éléments par **numéro**. Mais souvent, on veut retrouver une information par un **nom** : le numéro de téléphone de Bob, le prix d'une baguette, la capitale de l'Italie… C'est le rôle du **dictionnaire**.
**Analogie :** un vrai dictionnaire associe un **mot** (la clé) à sa **définition** (la valeur). On ne cherche pas « la définition n° 4127 », on cherche « le mot *chat* ».
```python
prix = {"pain": 1.20, "lait": 0.95, "oeufs": 2.50}
print(prix["lait"]) # → 0.95
print(len(prix)) # → 3
print(prix) # → {'pain': 1.2, 'lait': 0.95, 'oeufs': 2.5}
```
- On écrit un dictionnaire entre accolades `{ }`, avec des paires `cle: valeur` séparées par des virgules.
- Les **clés** sont uniques et doivent être immuables (chaînes, nombres, tuples). En pratique, ce sont presque toujours des chaînes.
- Les **valeurs** peuvent être de n'importe quel type, y compris des listes ou d'autres dictionnaires.
- Depuis Python 3.7, un dictionnaire **conserve l'ordre d'insertion**.
#### Ajouter, modifier, supprimer
```python
stock = {"pommes": 10, "poires": 4}
stock["kiwis"] = 7 # la clé n'existe pas : on l'AJOUTE
stock["pommes"] = 12 # la clé existe : on la MODIFIE
stock["poires"] += 3 # on peut utiliser += sur la valeur
print(stock) # → {'pommes': 12, 'poires': 7, 'kiwis': 7}
del stock["kiwis"] # suppression
print(stock) # → {'pommes': 12, 'poires': 7}
quantite = stock.pop("poires") # supprime ET renvoie la valeur
print(quantite) # → 7
print(stock) # → {'pommes': 12}
```
#### Lire sans risque : `in` et `get`
Accéder à une clé absente avec `[ ]` provoque une erreur `KeyError` :
```python
stock = {"pommes": 10}
print(stock["bananes"])
```
```text
Traceback (most recent call last):
File "<stdin>", line 2, in <module>
KeyError: 'bananes'
```
Deux solutions :
```python
stock = {"pommes": 10}
# 1. Vérifier avec in (in teste les CLÉS)
if "bananes" in stock:
print(stock["bananes"])
else:
print("Pas de bananes") # → Pas de bananes
# 2. Utiliser get(cle, valeur_par_defaut)
print(stock.get("bananes", 0)) # → 0
print(stock.get("pommes", 0)) # → 10
print(stock.get("bananes")) # → None
```
Sans valeur par défaut, `get` renvoie `None`, la valeur spéciale qui signifie « rien ».
#### Parcourir un dictionnaire
```python
capitales = {"France": "Paris", "Italie": "Rome", "Espagne": "Madrid"}
for pays in capitales: # par défaut : on parcourt les CLÉS
print(pays)
for ville in capitales.values(): # les valeurs
print(ville)
for pays, ville in capitales.items(): # les paires (clé, valeur)
print(f"La capitale de {pays} est {ville}.")
```
```text
France
Italie
Espagne
Paris
Rome
Madrid
La capitale de France est Paris.
La capitale de Italie est Rome.
La capitale de Espagne est Madrid.
```
`items()` renvoie des **tuples** `(cle, valeur)` que l'on déballe directement dans la boucle : c'est la forme que tu utiliseras le plus.
Les méthodes `keys()`, `values()` et `items()` renvoient des « vues » ; si tu as besoin d'une vraie liste, convertis avec `list()` :
```python
capitales = {"France": "Paris", "Italie": "Rome"}
print(list(capitales.keys())) # → ['France', 'Italie']
print(list(capitales.values())) # → ['Paris', 'Rome']
```
#### Un grand classique : compter avec un dictionnaire
Compter combien de fois apparaît chaque élément est un usage incontournable des dictionnaires.
```python
votes = ["chat", "chien", "chat", "lapin", "chat", "chien"]
compteur = {}
for animal in votes:
if animal in compteur:
compteur[animal] += 1
else:
compteur[animal] = 1
print(compteur) # → {'chat': 3, 'chien': 2, 'lapin': 1}
```
Avec `get`, c'est encore plus court :
```python
votes = ["chat", "chien", "chat", "lapin", "chat", "chien"]
compteur = {}
for animal in votes:
compteur[animal] = compteur.get(animal, 0) + 1
print(compteur) # → {'chat': 3, 'chien': 2, 'lapin': 1}
```
> Le module `collections` propose un outil tout prêt pour ça, `Counter`. Tu le découvriras au [chapitre 09](09-modules-bibliotheque-standard.md).
#### Dictionnaires imbriqués et listes de dictionnaires
Les structures se combinent. Une **liste de dictionnaires** est la façon standard de représenter un tableau de données (chaque dictionnaire est une ligne) :
```python
contacts = [
{"nom": "Alice", "tel": "06 11 22 33 44", "ville": "Lyon"},
{"nom": "Bob", "tel": "07 55 66 77 88", "ville": "Nantes"},
]
for contact in contacts:
print(f"{contact['nom']} ({contact['ville']}) : {contact['tel']}")
```
```text
Alice (Lyon) : 06 11 22 33 44
Bob (Nantes) : 07 55 66 77 88
```
> Dans une f-string délimitée par des guillemets `"…"`, utilise des apostrophes `'…'` pour les clés, afin de ne pas fermer la chaîne par erreur.
Et un **dictionnaire de listes** permet de regrouper plusieurs valeurs par clé :
```python
notes_par_eleve = {
"Alice": [14, 16, 12],
"Bob": [9, 12],
}
notes_par_eleve["Bob"].append(15)
notes_par_eleve["Chloé"] = [18]
for eleve, notes in notes_par_eleve.items():
print(f"{eleve} : {notes}, moyenne {sum(notes) / len(notes):.1f}")
```
```text
Alice : [14, 16, 12], moyenne 14.0
Bob : [9, 12, 15], moyenne 12.0
Chloé : [18], moyenne 18.0
```
---
### 12. Les ensembles (set) : des éléments uniques
Un **ensemble** est une collection **sans ordre** et **sans doublon**. On l'écrit entre accolades, mais sans paires `cle: valeur`.
```python
couleurs = {"rouge", "vert", "bleu", "rouge"}
print(len(couleurs)) # → 3
print("vert" in couleurs) # → True
```
Le doublon `"rouge"` a disparu automatiquement. Comme il n'y a pas d'ordre, **pas d'indice** : `couleurs[0]` provoque une erreur `TypeError`. L'ordre d'affichage d'un ensemble de chaînes peut même changer d'une exécution à l'autre ; pour un affichage stable, utilise `sorted(ensemble)`, qui renvoie une liste triée.
#### Créer, ajouter, retirer
```python
vide = set() # ATTENTION : {} crée un dictionnaire vide, pas un ensemble !
vide.add(3)
vide.add(1)
vide.add(3) # déjà présent : ne fait rien
print(sorted(vide)) # → [1, 3]
vide.discard(3) # retire 3 (aucune erreur s'il est absent)
vide.discard(42) # pas d'erreur
print(vide) # → {1}
```
`remove(x)` existe aussi, mais provoque une `KeyError` si `x` est absent ; `discard(x)` est plus tolérant.
#### Supprimer les doublons d'une liste
C'est l'usage le plus courant :
```python
emails = ["a@x.fr", "b@x.fr", "a@x.fr", "c@x.fr", "b@x.fr"]
uniques = set(emails)
print(len(uniques)) # → 3
print(sorted(uniques)) # → ['a@x.fr', 'b@x.fr', 'c@x.fr']
```
#### Les opérations ensemblistes
Comme en mathématiques, on peut combiner des ensembles :
```python
club_foot = {"Alice", "Bob", "Chloé", "David"}
club_theatre = {"Chloé", "Emma", "Bob", "Farid"}
print(sorted(club_foot | club_theatre)) # → ['Alice', 'Bob', 'Chloé', 'David', 'Emma', 'Farid']
print(sorted(club_foot & club_theatre)) # → ['Bob', 'Chloé']
print(sorted(club_foot - club_theatre)) # → ['Alice', 'David']
print(sorted(club_foot ^ club_theatre)) # → ['Alice', 'David', 'Emma', 'Farid']
```
| Opérateur | Nom | Résultat |
|---|---|---|
| `a \| b` | union | les éléments présents dans `a` **ou** `b` |
| `a & b` | intersection | les éléments présents dans `a` **et** `b` |
| `a - b` | différence | les éléments de `a` qui ne sont **pas** dans `b` |
| `a ^ b` | différence symétrique | les éléments présents dans **un seul** des deux |
**Bonus :** tester `x in ensemble` est extrêmement rapide, même avec des millions d'éléments, alors que `x in liste` doit parcourir toute la liste. Quand tu dois faire beaucoup de tests d'appartenance, pense aux ensembles.
---
### 13. Choisir la bonne structure
| Besoin | Structure |
|---|---|
| Une suite d'éléments dans un ordre précis, qui va évoluer | **liste** |
| Quelques valeurs liées qui ne doivent pas changer (coordonnées, date) | **tuple** |
| Retrouver une valeur à partir d'un nom/identifiant | **dictionnaire** |
| Des éléments uniques, tester l'appartenance, comparer deux groupes | **ensemble** |
Et le tableau des propriétés :
| | Ordonné | Modifiable | Doublons | Accès |
|---|---|---|---|---|
| `list` | oui | oui | autorisés | par indice `l[0]` |
| `tuple` | oui | non | autorisés | par indice `t[0]` |
| `dict` | oui (ordre d'insertion) | oui | clés uniques | par clé `d["cle"]` |
| `set` | non | oui | interdits | aucun (seulement `in`) |
Toutes ces structures fonctionnent avec `len()`, `in` et `for`. Et les conversions sont faciles : `list(...)`, `tuple(...)`, `set(...)`, `dict(...)`.
```python
lettres = list("bonjour")
print(lettres) # → ['b', 'o', 'n', 'j', 'o', 'u', 'r']
print(tuple([1, 2, 3])) # → (1, 2, 3)
print(sorted(set("bonjour"))) # → ['b', 'j', 'n', 'o', 'r', 'u']
print(dict([("a", 1), ("b", 2)])) # → {'a': 1, 'b': 2}
```
---
## Pièges fréquents
### Piège 1 — Croire que `b = a` copie une liste
```python
originale = ["pain", "lait"]
sauvegarde = originale # pas une copie : un deuxième nom !
originale.append("beurre")
print(sauvegarde) # → ['pain', 'lait', 'beurre']
```
Aucun message d'erreur : c'est ce qui rend ce bug sournois. **Correction :**
```python
originale = ["pain", "lait"]
sauvegarde = originale.copy() # vraie copie
originale.append("beurre")
print(sauvegarde) # → ['pain', 'lait']
```
### Piège 2 — Modifier une liste pendant qu'on la parcourt
On veut retirer toutes les notes inférieures à 10 :
```python
notes = [8, 5, 12, 9, 15]
for note in notes:
if note < 10:
notes.remove(note)
print(notes) # → [5, 12, 15]
```
Le 5 est resté ! En retirant le 8, tous les éléments ont « glissé » d'une case vers la gauche, et la boucle a sauté le 5. **Correction :** construire une nouvelle liste (ou parcourir une copie).
```python
notes = [8, 5, 12, 9, 15]
notes_gardees = []
for note in notes:
if note >= 10:
notes_gardees.append(note)
notes = notes_gardees
print(notes) # → [12, 15]
```
Pour un dictionnaire, c'est encore plus strict : Python refuse carrément.
```python
stock = {"pommes": 0, "poires": 4}
for fruit in stock:
if stock[fruit] == 0:
del stock[fruit]
```
```text
Traceback (most recent call last):
File "<stdin>", line 4, in <module>
RuntimeError: dictionary changed size during iteration
```
**Correction :** parcourir une copie des clés, `for fruit in list(stock):`.
```python
stock = {"pommes": 0, "poires": 4}
for fruit in list(stock):
if stock[fruit] == 0:
del stock[fruit]
print(stock) # → {'poires': 4}
```
### Piège 3 — `liste = liste.sort()`
```python
nombres = [3, 1, 2]
nombres = nombres.sort()
print(nombres) # → None
```
`sort()` trie la liste **sur place** et renvoie `None`. En réaffectant, tu as perdu ta liste. **Correction :** `nombres.sort()` tout seul, ou `nombres = sorted(nombres)`.
### Piège 4 — Sortir des indices de la liste
```python
nombres = [10, 20, 30]
for i in range(len(nombres) + 1):
print(nombres[i])
```
```text
10
20
30
Traceback (most recent call last):
File "<stdin>", line 3, in <module>
IndexError: list index out of range
```
Le dernier indice valide est `len(nombres) - 1`. **Correction :** `range(len(nombres))`, ou mieux, `for nombre in nombres:`.
### Piège 5 — Accéder à une clé absente d'un dictionnaire
```python
age = {"Alice": 30}
print(age["alice"]) # attention à la casse !
```
```text
Traceback (most recent call last):
File "<stdin>", line 2, in <module>
KeyError: 'alice'
```
**Correction :** vérifier avec `if "alice" in age:` ou utiliser `age.get("alice", "inconnu")`. Et rappelle-toi que `"Alice"` et `"alice"` sont deux clés différentes.
### Piège 6 — `{}` n'est pas un ensemble vide
```python
deja_vus = {}
deja_vus.add("Paris")
```
```text
Traceback (most recent call last):
File "<stdin>", line 2, in <module>
AttributeError: 'dict' object has no attribute 'add'
```
**Correction :** `deja_vus = set()`.
### Piège 7 — `append` au lieu de `extend`
```python
liste = [1, 2]
liste.append([3, 4])
print(liste) # → [1, 2, [3, 4]]
```
`append` ajoute **un** élément (ici, une liste entière). **Correction :** `liste.extend([3, 4])` donne `[1, 2, 3, 4]`.
### Piège 8 — Créer une grille avec `*`
```python
grille = [[0] * 3] * 3
grille[0][0] = 1
print(grille) # → [[1, 0, 0], [1, 0, 0], [1, 0, 0]]
```
Les trois lignes sont **la même** liste (trois étiquettes sur un seul objet, cf. section 9). **Correction :** créer chaque ligne séparément dans une boucle.
```python
grille = []
for _ in range(3):
grille.append([0] * 3)
grille[0][0] = 1
print(grille) # → [[1, 0, 0], [0, 0, 0], [0, 0, 0]]
```
> Le nom de variable `_` est une convention qui signifie « je n'utilise pas cette variable ».
---
## Exercices
### Exercice 1 — Statistiques de notes ⭐
On donne `notes = [12, 8, 15, 19, 7, 11, 14]`. Affiche le nombre de notes, la plus petite, la plus grande, la moyenne arrondie à 2 décimales, et le nombre de notes supérieures ou égales à 10.
Sortie attendue :
```text
Nombre de notes : 7
Minimum : 7
Maximum : 19
Moyenne : 12.29
Notes >= 10 : 5
```
### Exercice 2 — Manipulations de liste ⭐
Pars de `animaux = ["chat", "chien", "lapin"]` et, dans l'ordre :
1. ajoute `"hamster"` à la fin ;
2. insère `"poisson"` en première position ;
3. retire `"chien"` ;
4. affiche la liste, son premier et son dernier élément ;
5. affiche la liste triée par ordre alphabétique, puis la liste à l'envers (sans la trier).
Sortie attendue :
```text
['poisson', 'chat', 'lapin', 'hamster']
Premier : poisson
Dernier : hamster
Triée : ['chat', 'hamster', 'lapin', 'poisson']
À l'envers : ['hamster', 'lapin', 'chat', 'poisson']
```
### Exercice 3 — Compter les mots ⭐⭐
On donne la phrase `phrase = "le chat mange le poisson et le chien mange le chat"`. La méthode `phrase.split()` découpe une chaîne en liste de mots (on la reverra au chapitre 06). Construis un dictionnaire qui associe chaque mot à son nombre d'apparitions, puis affiche chaque mot avec son compte, et enfin le mot le plus fréquent.
Sortie attendue :
```text
le : 4
chat : 2
mange : 2
poisson : 1
et : 1
chien : 1
Mot le plus fréquent : 'le' (4 fois)
```
### Exercice 4 — Petit annuaire ⭐⭐
On donne :
```python
annuaire = {"Alice": "06 12 34 56 78", "Bob": "07 98 76 54 32", "Chloé": "06 55 44 33 22"}
```
Demande à l'utilisateur des prénoms en boucle (vide pour arrêter). Pour chaque prénom, affiche le numéro s'il existe, sinon propose de l'ajouter en demandant le numéro. À la fin, affiche le nombre de contacts.
Exemple d'exécution :
```text
Prénom (vide pour quitter) : Bob
Bob : 07 98 76 54 32
Prénom (vide pour quitter) : David
David est inconnu. Son numéro : 06 00 00 00 00
David ajouté.
Prénom (vide pour quitter) :
L'annuaire contient 4 contacts.
```
### Exercice 5 — Amis en commun ⭐⭐
On donne les listes d'amis de deux personnes (avec des doublons dus à une erreur de saisie) :
```python
amis_lea = ["Tom", "Inès", "Hugo", "Tom", "Sarah", "Yanis"]
amis_noe = ["Sarah", "Lina", "Tom", "Hugo", "Lina"]
```
Affiche (toujours triés par ordre alphabétique) : les amis communs, les amis de Léa que Noé ne connaît pas, et tous les amis sans doublon, avec leur nombre.
Sortie attendue :
```text
Amis communs : ['Hugo', 'Sarah', 'Tom']
Seulement chez Léa : ['Inès', 'Yanis']
Tous (6) : ['Hugo', 'Inès', 'Lina', 'Sarah', 'Tom', 'Yanis']
```
### Exercice 6 — Transposer une grille ⭐⭐
On donne une grille de 2 lignes et 3 colonnes :
```python
grille = [
[1, 2, 3],
[4, 5, 6],
]
```
Construis sa **transposée** (les lignes deviennent les colonnes), de 3 lignes et 2 colonnes, avec des boucles, puis affiche-la ligne par ligne.
Sortie attendue :
```text
[1, 4]
[2, 5]
[3, 6]
```
### Exercice 7 — Gestion de stock ⭐⭐⭐
Un magasin a ce stock et reçoit cette liste de commandes (tuples `(article, quantité)`) :
```python
stock = {"stylo": 50, "cahier": 20, "gomme": 5, "règle": 0}
commandes = [("stylo", 10), ("gomme", 8), ("cahier", 20), ("règle", 1), ("trousse", 2)]
```
Traite les commandes dans l'ordre :
- si l'article n'existe pas, affiche `trousse : article inconnu` ;
- si le stock est insuffisant, affiche `gomme : stock insuffisant (5 disponible(s))` et ne retire rien ;
- sinon, retire la quantité et affiche `stylo : 10 expédié(s)`.
Enfin, affiche le stock final, puis la liste triée des articles en rupture (quantité 0).
Sortie attendue :
```text
stylo : 10 expédié(s)
gomme : stock insuffisant (5 disponible(s))
cahier : 20 expédié(s)
règle : stock insuffisant (0 disponible(s))
trousse : article inconnu
Stock final : {'stylo': 40, 'cahier': 0, 'gomme': 5, 'règle': 0}
En rupture : ['cahier', 'règle']
```
### Exercice 8 — Classement des joueurs ⭐⭐⭐
On donne les scores d'une partie :
```python
scores = {"Léo": 1200, "Maya": 1850, "Sam": 900, "Inès": 1850, "Noah": 1500}
```
Affiche le classement du meilleur au moins bon score. En cas d'égalité, classe par ordre alphabétique **et donne le même rang** aux ex æquo.
Indice : construis une liste de tuples `(score, nom)` et trie-la. Python compare les tuples élément par élément : d'abord le score, puis le nom en cas d'égalité. Comme on veut les scores décroissants mais les noms croissants, une astuce consiste à stocker `-score`.
Sortie attendue :
```text
1. Inès - 1850 pts
1. Maya - 1850 pts
3. Noah - 1500 pts
4. Léo - 1200 pts
5. Sam - 900 pts
```
---
## Mini-projet — Le gestionnaire de liste de courses
### Cahier des charges
Écris un programme interactif qui gère une liste de courses. Les articles sont stockés dans un **dictionnaire** `{article: quantite}`. Le programme affiche un menu en boucle :
```text
=== LISTE DE COURSES ===
1. Ajouter un article
2. Retirer un article
3. Afficher la liste
4. Vider la liste
5. Quitter
```
- **Ajouter** : demander le nom et la quantité. Si l'article existe déjà, **additionner** les quantités. Les noms sont enregistrés en minuscules (`saisie.lower()`) et sans espaces autour (`saisie.strip()`) pour que « Pain » et « pain » soient le même article.
- **Retirer** : demander le nom ; s'il existe, le supprimer, sinon afficher un message.
- **Afficher** : lister les articles **par ordre alphabétique**, avec leur quantité, et le nombre total d'articles. Si la liste est vide, le dire.
- **Vider** : tout supprimer.
- **Quitter** : afficher un message d'au revoir et arrêter.
- Un choix invalide affiche un message d'erreur sans planter.
Exemple d'exécution :
```text
Ton choix : 1
Article : Pain
Quantité : 2
2 x pain ajouté(s).
Ton choix : 1
Article : pain
Quantité : 1
1 x pain ajouté(s).
Ton choix : 3
- pain : 3
Total : 3 article(s)
```
### Étapes suggérées
1. Crée le dictionnaire vide et une boucle `while True` qui affiche le menu et lit le choix.
2. Gère le choix `"5"` avec `break`, puis un `else` pour les choix invalides.
3. Implémente « Afficher » (avec `sorted(courses)` pour l'ordre alphabétique).
4. Implémente « Ajouter » avec `courses.get(article, 0) + quantite`.
5. Implémente « Retirer » et « Vider ».
6. Vérifie que la quantité saisie est bien un nombre avec `saisie.isdigit()` (renvoie `True` si la chaîne ne contient que des chiffres) avant d'appeler `int()`.
### Pistes d'amélioration
- Permettre de retirer seulement **une partie** de la quantité (et supprimer l'article si on tombe à 0).
- Ajouter un **prix unitaire** : dictionnaire de dictionnaires `{"pain": {"quantite": 2, "prix": 1.20}}` et afficher le total à payer.
- Classer les articles par **rayon** (`{"boulangerie": {...}, "frais": {...}}`).
- Utiliser `match/case` (chapitre 02) pour le menu.
- Plus tard : découper le programme en fonctions ([chapitre 05](05-fonctions.md)) et sauvegarder la liste dans un fichier JSON ([chapitre 07](07-fichiers-csv-json.md)).
---
## Corrigés
<details>
<summary>Corrigé de l'exercice 1</summary>
```python
notes = [12, 8, 15, 19, 7, 11, 14]
nb_admis = 0
for note in notes:
if note >= 10:
nb_admis += 1
moyenne = sum(notes) / len(notes)
print(f"Nombre de notes : {len(notes)}")
print(f"Minimum : {min(notes)}")
print(f"Maximum : {max(notes)}")
print(f"Moyenne : {moyenne:.2f}")
print(f"Notes >= 10 : {nb_admis}")
```
Sortie :
```text
Nombre de notes : 7
Minimum : 7
Maximum : 19
Moyenne : 12.29
Notes >= 10 : 5
```
`min`, `max`, `sum` et `len` font l'essentiel du travail. Le comptage des notes admises utilise le schéma « compteur » : une variable à 0 incrémentée dans la boucle. La f-string `{moyenne:.2f}` arrondit l'affichage à 2 décimales (86 / 7 = 12.2857…).
</details>
<details>
<summary>Corrigé de l'exercice 2</summary>
```python
animaux = ["chat", "chien", "lapin"]
animaux.append("hamster")
animaux.insert(0, "poisson")
animaux.remove("chien")
print(animaux)
print(f"Premier : {animaux[0]}")
print(f"Dernier : {animaux[-1]}")
print(f"Triée : {sorted(animaux)}")
print(f"À l'envers : {animaux[::-1]}")
```
Sortie :
```text
['poisson', 'chat', 'lapin', 'hamster']
Premier : poisson
Dernier : hamster
Triée : ['chat', 'hamster', 'lapin', 'poisson']
À l'envers : ['hamster', 'lapin', 'chat', 'poisson']
```
On utilise `sorted()` et `[::-1]` plutôt que `sort()` et `reverse()` : ainsi, la liste `animaux` n'est pas modifiée par les affichages. L'indice `-1` donne le dernier élément quelle que soit la longueur.
</details>
<details>
<summary>Corrigé de l'exercice 3</summary>
```python
phrase = "le chat mange le poisson et le chien mange le chat"
compteur = {}
for mot in phrase.split():
compteur[mot] = compteur.get(mot, 0) + 1
for mot, nombre in compteur.items():
print(f"{mot} : {nombre}")
# Recherche du maximum « à la main »
mot_max = ""
nombre_max = 0
for mot, nombre in compteur.items():
if nombre > nombre_max:
mot_max = mot
nombre_max = nombre
print(f"Mot le plus fréquent : '{mot_max}' ({nombre_max} fois)")
```
Sortie :
```text
le : 4
chat : 2
mange : 2
poisson : 1
et : 1
chien : 1
Mot le plus fréquent : 'le' (4 fois)
```
`compteur.get(mot, 0)` renvoie le compte actuel, ou 0 si le mot n'a jamais été vu. Le dictionnaire conserve l'ordre d'insertion, d'où l'ordre d'affichage. Pour le maximum, on garde en mémoire le meilleur trouvé jusqu'ici et on le remplace dès qu'on trouve mieux.
</details>
<details>
<summary>Corrigé de l'exercice 4</summary>
```python
annuaire = {"Alice": "06 12 34 56 78", "Bob": "07 98 76 54 32", "Chloé": "06 55 44 33 22"}
prenom = input("Prénom (vide pour quitter) : ")
while prenom != "":
if prenom in annuaire:
print(f"{prenom} : {annuaire[prenom]}")
else:
numero = input(f"{prenom} est inconnu. Son numéro : ")
annuaire[prenom] = numero
print(f"{prenom} ajouté.")
prenom = input("Prénom (vide pour quitter) : ")
print(f"L'annuaire contient {len(annuaire)} contacts.")
```
Avec les saisies `Bob`, `David`, `06 00 00 00 00` puis une ligne vide :
```text
Prénom (vide pour quitter) : Bob
Bob : 07 98 76 54 32
Prénom (vide pour quitter) : David
David est inconnu. Son numéro : 06 00 00 00 00
David ajouté.
Prénom (vide pour quitter) :
L'annuaire contient 4 contacts.
```
On teste d'abord `prenom in annuaire` pour éviter une `KeyError`. Ajouter un contact, c'est simplement affecter une nouvelle clé : `annuaire[prenom] = numero`.
</details>
<details>
<summary>Corrigé de l'exercice 5</summary>
```python
amis_lea = ["Tom", "Inès", "Hugo", "Tom", "Sarah", "Yanis"]
amis_noe = ["Sarah", "Lina", "Tom", "Hugo", "Lina"]
ens_lea = set(amis_lea)
ens_noe = set(amis_noe)
communs = ens_lea & ens_noe
seulement_lea = ens_lea - ens_noe
tous = ens_lea | ens_noe
print(f"Amis communs : {sorted(communs)}")
print(f"Seulement chez Léa : {sorted(seulement_lea)}")
print(f"Tous ({len(tous)}) : {sorted(tous)}")
```
Sortie :
```text
Amis communs : ['Hugo', 'Sarah', 'Tom']
Seulement chez Léa : ['Inès', 'Yanis']
Tous (6) : ['Hugo', 'Inès', 'Lina', 'Sarah', 'Tom', 'Yanis']
```
Convertir en `set` élimine les doublons d'un coup, puis les opérateurs `&`, `-` et `|` répondent directement aux questions. `sorted()` transforme l'ensemble en liste triée pour un affichage stable.
</details>
<details>
<summary>Corrigé de l'exercice 6</summary>
```python
grille = [
[1, 2, 3],
[4, 5, 6],
]
nb_lignes = len(grille)
nb_colonnes = len(grille[0])
transposee = []
for j in range(nb_colonnes): # chaque colonne de la grille...
nouvelle_ligne = []
for i in range(nb_lignes): # ...devient une ligne de la transposée
nouvelle_ligne.append(grille[i][j])
transposee.append(nouvelle_ligne)
for ligne in transposee:
print(ligne)
```
Sortie :
```text
[1, 4]
[2, 5]
[3, 6]
```
L'élément en ligne `i`, colonne `j` de la grille se retrouve en ligne `j`, colonne `i` de la transposée. La boucle extérieure parcourt donc les colonnes, et on crée une **nouvelle** liste à chaque tour (pas de piège 8 ici).
</details>
<details>
<summary>Corrigé de l'exercice 7</summary>
```python
stock = {"stylo": 50, "cahier": 20, "gomme": 5, "règle": 0}
commandes = [("stylo", 10), ("gomme", 8), ("cahier", 20), ("règle", 1), ("trousse", 2)]
for article, quantite in commandes:
if article not in stock:
print(f"{article} : article inconnu")
elif stock[article] < quantite:
print(f"{article} : stock insuffisant ({stock[article]} disponible(s))")
else:
stock[article] -= quantite
print(f"{article} : {quantite} expédié(s)")
print(f"Stock final : {stock}")
ruptures = []
for article, quantite in stock.items():
if quantite == 0:
ruptures.append(article)
ruptures.sort()
print(f"En rupture : {ruptures}")
```
Sortie :
```text
stylo : 10 expédié(s)
gomme : stock insuffisant (5 disponible(s))
cahier : 20 expédié(s)
règle : stock insuffisant (0 disponible(s))
trousse : article inconnu
Stock final : {'stylo': 40, 'cahier': 0, 'gomme': 5, 'règle': 0}
En rupture : ['cahier', 'règle']
```
Le déballage `for article, quantite in commandes` rend la boucle très lisible. L'ordre des tests compte : on vérifie d'abord que l'article existe (sinon `stock[article]` provoquerait une `KeyError`), puis que la quantité suffit.
</details>
<details>
<summary>Corrigé de l'exercice 8</summary>
```python
scores = {"Léo": 1200, "Maya": 1850, "Sam": 900, "Inès": 1850, "Noah": 1500}
# 1. Liste de tuples (-score, nom) : le tri croissant donnera
# les scores décroissants, puis les noms par ordre alphabétique.
classement = []
for nom, score in scores.items():
classement.append((-score, nom))
classement.sort()
# 2. Affichage avec gestion des ex æquo
rang = 0
score_precedent = None
for position in range(len(classement)):
score_negatif, nom = classement[position]
score = -score_negatif
if score != score_precedent:
rang = position + 1 # nouveau score : le rang = la position réelle
print(f"{rang}. {nom} - {score} pts")
score_precedent = score
```
Sortie :
```text
1. Inès - 1850 pts
1. Maya - 1850 pts
3. Noah - 1500 pts
4. Léo - 1200 pts
5. Sam - 900 pts
```
Les tuples se comparent comme des mots dans le dictionnaire : premier élément d'abord, puis le suivant en cas d'égalité. En stockant `-1850`, `-1500`, etc., le plus grand score devient le plus petit nombre et arrive en tête du tri croissant. Pour les ex æquo, le rang ne change que lorsque le score change, et il saute alors à la position réelle (d'où le « 3 » après deux « 1 », comme dans un vrai classement sportif). Au chapitre 06, tu verras une écriture plus directe avec `sorted(..., key=lambda ...)`.
</details>
<details>
<summary>Corrigé du mini-projet</summary>
```python
courses = {}
while True:
print()
print("=== LISTE DE COURSES ===")
print("1. Ajouter un article")
print("2. Retirer un article")
print("3. Afficher la liste")
print("4. Vider la liste")
print("5. Quitter")
choix = input("Ton choix : ").strip()
if choix == "1":
article = input("Article : ").strip().lower()
saisie = input("Quantité : ").strip()
if article == "":
print("Nom d'article vide, rien n'a été ajouté.")
elif not saisie.isdigit() or int(saisie) == 0:
print("La quantité doit être un nombre entier positif.")
else:
quantite = int(saisie)
courses[article] = courses.get(article, 0) + quantite
print(f"{quantite} x {article} ajouté(s).")
elif choix == "2":
article = input("Article à retirer : ").strip().lower()
if article in courses:
del courses[article]
print(f"{article} retiré de la liste.")
else:
print(f"{article} n'est pas dans la liste.")
elif choix == "3":
if len(courses) == 0:
print("La liste est vide.")
else:
total = 0
for article in sorted(courses):
print(f"- {article} : {courses[article]}")
total += courses[article]
print(f"Total : {total} article(s)")
elif choix == "4":
courses.clear()
print("Liste vidée.")
elif choix == "5":
print("Bonnes courses, à bientôt !")
break
else:
print("Choix invalide, tape un nombre entre 1 et 5.")
```
Exemple de session (saisies : `1`, `Pain`, `2`, `1`, `pain`, `1`, `1`, `Tomates`, `6`, `3`, `2`, `beurre`, `7`, `5`) :
```text
=== LISTE DE COURSES ===
1. Ajouter un article
2. Retirer un article
3. Afficher la liste
4. Vider la liste
5. Quitter
Ton choix : 1
Article : Pain
Quantité : 2
2 x pain ajouté(s).
=== LISTE DE COURSES ===
1. Ajouter un article
2. Retirer un article
3. Afficher la liste
4. Vider la liste
5. Quitter
Ton choix : 1
Article : pain
Quantité : 1
1 x pain ajouté(s).
=== LISTE DE COURSES ===
1. Ajouter un article
2. Retirer un article
3. Afficher la liste
4. Vider la liste
5. Quitter
Ton choix : 1
Article : Tomates
Quantité : 6
6 x tomates ajouté(s).
=== LISTE DE COURSES ===
1. Ajouter un article
2. Retirer un article
3. Afficher la liste
4. Vider la liste
5. Quitter
Ton choix : 3
- pain : 3
- tomates : 6
Total : 9 article(s)
=== LISTE DE COURSES ===
1. Ajouter un article
2. Retirer un article
3. Afficher la liste
4. Vider la liste
5. Quitter
Ton choix : 2
Article à retirer : beurre
beurre n'est pas dans la liste.
=== LISTE DE COURSES ===
1. Ajouter un article
2. Retirer un article
3. Afficher la liste
4. Vider la liste
5. Quitter
Ton choix : 7
Choix invalide, tape un nombre entre 1 et 5.
=== LISTE DE COURSES ===
1. Ajouter un article
2. Retirer un article
3. Afficher la liste
4. Vider la liste
5. Quitter
Ton choix : 5
Bonnes courses, à bientôt !
```
**Points clés de la solution :**
- `while True` + `break` est le schéma classique d'un menu : on boucle « pour toujours » jusqu'à ce que l'utilisateur choisisse de quitter.
- `.strip().lower()` normalise les noms : « Pain », « pain » et « PAIN » désignent le même article.
- `courses.get(article, 0) + quantite` gère d'un coup le cas « nouvel article » et le cas « article déjà présent ».
- `saisie.isdigit()` est vérifié **avant** `int(saisie)` : si la saisie contient des lettres, `int()` provoquerait une erreur. Grâce à l'évaluation paresseuse de `or` (chapitre 02), `int(saisie)` n'est jamais appelé si `isdigit()` est faux. Au [chapitre 08](08-erreurs-et-debogage.md), tu verras une autre approche avec `try/except`.
- `sorted(courses)` renvoie les clés triées sans modifier le dictionnaire.
</details>
---
## Récapitulatif
- Une **liste** `[a, b, c]` est ordonnée et modifiable ; on accède aux éléments par **indice** à partir de 0 (`-1` pour le dernier).
- Méthodes essentielles : `append`, `insert`, `extend`, `remove`, `pop`, `sort`, `reverse`, `index`, `count`, `copy`, `clear`.
- Fonctions utiles : `len`, `min`, `max`, `sum`, `sorted`, et le test `in`.
- Le **slicing** `liste[debut:fin:pas]` extrait une portion (fin exclue).
- `b = a` ne copie pas : les deux noms désignent **la même** liste. Pour copier : `a.copy()`, `list(a)` ou `a[:]` (et `copy.deepcopy` pour les listes imbriquées).
- Ne modifie jamais une collection pendant que tu la parcours : construis-en une nouvelle.
- Un **tuple** `(a, b)` est immuable ; on le **déballe** avec `x, y = point`. Un tuple à un élément s'écrit `(x,)`.
- Un **dictionnaire** `{cle: valeur}` permet de retrouver une valeur par sa clé ; `get(cle, defaut)` évite les `KeyError`, `items()` permet de parcourir les paires.
- Un **ensemble** `{a, b}` ne contient pas de doublons ; il se crée vide avec `set()` et supporte `|`, `&`, `-`, `^`.
### Checklist de validation
- [ ] Je sais créer une liste, lire `liste[0]` et `liste[-1]`, et la modifier.
- [ ] Je connais la différence entre `append` et `extend`, entre `sort()` et `sorted()`.
- [ ] Je sais expliquer pourquoi `b = a` ne copie pas une liste, et comment faire une vraie copie.
- [ ] Je sais déballer un tuple et échanger deux variables en une ligne.
- [ ] Je sais compter des occurrences avec un dictionnaire et `get`.
- [ ] Je sais parcourir un dictionnaire avec `items()`.
- [ ] Je sais supprimer les doublons d'une liste et trouver les éléments communs à deux listes.
- [ ] J'ai terminé le mini-projet « liste de courses ».
---
[← Chapitre précédent](03-boucles.md) | [Sommaire](README.md) | [Chapitre suivant →](05-fonctions.md)