alpinux-wiki/docs/presentations/Python/06-chaines-et-comprehensions.md
2026-10-15 00:28:56 +02:00

1598 lines
51 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

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

# Chapitre 06 — Chaînes de caractères et compréhensions
> **Durée estimée :** 5 à 6 heures (cours, exercices et mini-projet)
>
> **Prérequis :** [Chapitre 05 — Les fonctions](05-fonctions.md) (`def`, paramètres, `return`)
>
> **Objectifs — à la fin de ce chapitre, tu sauras…**
>
> - manipuler du texte avec les principales **méthodes de `str`** (`split`, `join`, `strip`, `replace`, `find`…) ;
> - extraire des morceaux de chaîne avec le **slicing** ;
> - écrire des **compréhensions** de listes, de dictionnaires et d'ensembles ;
> - parcourir plus élégamment avec **`enumerate`** et **`zip`** ;
> - trier finement avec **`sorted(..., key=...)`** et des fonctions **`lambda`** ;
> - nettoyer et analyser un texte complet.
---
## Cours
### 1. Rappels : une chaîne est une séquence
Tu manipules des chaînes (`str`) depuis le [chapitre 01](01-variables-types-operateurs.md). Il est temps de voir qu'une chaîne se comporte **comme une liste de caractères** (mais une liste figée, comme un tuple).
```python
mot = "Python"
print(len(mot)) # → 6
print(mot[0]) # → P
print(mot[-1]) # → n
print("y" in mot) # → True
print("thon" in mot) # → True
for lettre in mot:
print(lettre, end=" ")
print()
```
```text
6
P
n
True
True
P y t h o n
```
`in` sur une chaîne cherche une **sous-chaîne** entière : `"thon" in "Python"` est vrai.
#### Les chaînes sont immuables
Comme les tuples, on ne peut pas modifier une chaîne « sur place » :
```python
mot = "Python"
mot[0] = "J"
```
```text
Traceback (most recent call last):
File "<stdin>", line 2, in <module>
TypeError: 'str' object does not support item assignment
```
Toutes les « modifications » de chaîne créent en fait une **nouvelle** chaîne :
```python
mot = "Python"
mot = "J" + mot[1:]
print(mot) # → Jython
```
C'est fondamental pour comprendre les méthodes qui suivent : **aucune ne modifie la chaîne d'origine**, elles **renvoient** une nouvelle chaîne. Il faut donc récupérer le résultat.
```python
nom = "alice"
nom.upper() # le résultat est perdu !
print(nom) # → alice
nom = nom.upper()
print(nom) # → ALICE
```
#### Caractères spéciaux et chaînes sur plusieurs lignes
```python
print("Ligne 1\nLigne 2") # \n = retour à la ligne
print("Col1\tCol2") # \t = tabulation
print("Il a dit \"bonjour\"") # \" = guillemet dans la chaîne
print('C\'est facile') # \' = apostrophe
print("C:\\Users\\alice") # \\ = un antislash
```
```text
Ligne 1
Ligne 2
Col1 Col2
Il a dit "bonjour"
C'est facile
C:\Users\alice
```
Les triples guillemets permettent d'écrire du texte sur plusieurs lignes :
```python
poeme = """Mignonne, allons voir si la rose
Qui ce matin avait déclose
Sa robe de pourpre au soleil"""
print(len(poeme.splitlines())) # → 3
```
---
### 2. Le slicing en détail
Tu as découvert le slicing sur les listes au [chapitre 04](04-structures-de-donnees.md). Il fonctionne exactement pareil sur les chaînes : `chaine[debut:fin:pas]`, avec `fin` **exclu**.
```text
indice positif : 0 1 2 3 4 5 6 7 8 9
caractère : B o n j o u r ! ! !
indice négatif : -10 -9 -8 -7 -6 -5 -4 -3 -2 -1
```
```python
texte = "Bonjour!!!"
print(texte[0:3]) # → Bon
print(texte[3:7]) # → jour
print(texte[:7]) # → Bonjour
print(texte[7:]) # → !!!
print(texte[-3:]) # → !!!
print(texte[:-3]) # → Bonjour
print(texte[::2]) # → Bnor!
print(texte[::-1]) # → !!!ruojnoB
```
Astuce pour retenir : imagine que les indices désignent les **séparations entre les caractères**, et non les caractères eux-mêmes. `texte[3:7]` prend ce qui se trouve entre la séparation 3 et la séparation 7.
```text
| B | o | n | j | o | u | r | ! | ! | ! |
0 1 2 3 4 5 6 7 8 9 10
```
Contrairement à l'accès par indice, le slicing ne provoque **jamais** d'erreur si on dépasse :
```python
mot = "chat"
print(mot[2:100]) # → at
print(mot[10:] == "") # → True (on obtient une chaîne vide)
```
Quelques usages typiques :
```python
fichier = "rapport_2024.pdf"
print(fichier[-4:]) # → .pdf (l'extension)
print(fichier[:-4]) # → rapport_2024 (sans l'extension)
code_postal = "75011"
print(code_postal[:2]) # → 75 (le département)
mot = "kayak"
print(mot == mot[::-1]) # → True (palindrome en une ligne !)
```
---
### 3. Les méthodes de chaîne essentielles
Une méthode s'appelle avec un point : `chaine.methode(...)`. Voici celles que tu utiliseras tout le temps, regroupées par usage.
#### Changer la casse
```python
titre = "le Petit PRINCE"
print(titre.upper()) # → LE PETIT PRINCE
print(titre.lower()) # → le petit prince
print(titre.capitalize()) # → Le petit prince
print(titre.title()) # → Le Petit Prince
print(titre.swapcase()) # → LE pETIT prince
```
Pour comparer deux textes sans tenir compte des majuscules, on les met tous les deux en minuscules :
```python
reponse = "OUI"
if reponse.lower() == "oui":
print("D'accord !") # → D'accord !
```
#### Nettoyer les espaces
`strip()` retire les espaces (et retours à la ligne, tabulations) **au début et à la fin**. C'est indispensable pour nettoyer une saisie utilisateur.
```python
saisie = " alice@mail.fr \n"
print(f"[{saisie.strip()}]") # → [alice@mail.fr]
```
`lstrip()` et `rstrip()` ne nettoient qu'un seul côté (gauche ou droite) :
```python
saisie = " bonjour "
print(f"[{saisie.strip()}]") # → [bonjour]
print(f"[{saisie.lstrip()}]") # → [bonjour ]
print(f"[{saisie.rstrip()}]") # → [ bonjour]
print("---titre---".strip("-")) # → titre
```
On peut passer à `strip` les caractères à retirer, comme dans la dernière ligne.
#### Chercher
```python
phrase = "Le chat dort. Le chat mange."
print(phrase.find("chat")) # → 3 (indice de la 1re occurrence)
print(phrase.find("chien")) # → -1 (pas trouvé)
print(phrase.count("chat")) # → 2
print(phrase.startswith("Le")) # → True
print(phrase.endswith(".")) # → True
print(phrase.index("dort")) # → 8
```
`find` renvoie `-1` si rien n'est trouvé, alors que `index` provoque une erreur `ValueError: substring not found`. Pour un simple test de présence, `in` reste le plus lisible.
#### Remplacer
```python
phrase = "J'aime le thé. Le thé c'est bon."
print(phrase.replace("thé", "café")) # → J'aime le café. Le café c'est bon.
print(phrase.replace("thé", "café", 1)) # → J'aime le café. Le thé c'est bon.
print(phrase.replace(".", "")) # → J'aime le thé Le thé c'est bon
```
Le troisième argument limite le nombre de remplacements. Remplacer par `""` revient à **supprimer**.
#### Tester le contenu
Ces méthodes renvoient un booléen :
```python
print("2024".isdigit()) # → True (que des chiffres)
print("12.5".isdigit()) # → False (le point n'est pas un chiffre)
print("Bonjour".isalpha()) # → True (que des lettres)
print("Été".isalpha()) # → True (les accents sont des lettres)
print("abc123".isalnum()) # → True (lettres ou chiffres)
print(" ".isspace()) # → True
print("ALICE".isupper()) # → True
print("alice".islower()) # → True
```
#### Découper avec `split` et assembler avec `join`
Ce sont les deux méthodes les plus importantes du chapitre.
`split()` découpe une chaîne en **liste** :
```python
phrase = "le chat dort"
print(phrase.split()) # → ['le', 'chat', 'dort']
date = "14/07/1789"
print(date.split("/")) # → ['14', '07', '1789']
ligne_csv = "Alice;30;Lyon"
nom, age, ville = ligne_csv.split(";")
print(nom, age, ville) # → Alice 30 Lyon
print("a,b,,c".split(",")) # → ['a', 'b', '', 'c']
```
Sans argument, `split()` coupe sur **n'importe quel blanc** (espaces multiples, tabulations, retours à la ligne) et ignore les blancs en trop. Avec un séparateur, il coupe exactement sur ce séparateur, d'où la chaîne vide `''` dans le dernier exemple.
`join()` fait l'inverse : il **assemble** une liste de chaînes en une seule, en insérant un séparateur. Attention à la syntaxe, c'est le **séparateur** qui appelle la méthode :
```python
mots = ["le", "chat", "dort"]
print(" ".join(mots)) # → le chat dort
print("-".join(mots)) # → le-chat-dort
print("".join(mots)) # → lechatdort
print(", ".join(mots)) # → le, chat, dort
print("\n".join(mots))
```
```text
le chat dort
le-chat-dort
lechatdort
le, chat, dort
le
chat
dort
```
`join` n'accepte **que des chaînes** :
```python
nombres = [1, 2, 3]
print("-".join(nombres))
```
```text
Traceback (most recent call last):
File "<stdin>", line 2, in <module>
TypeError: sequence item 0: expected str instance, int found
```
Il faut d'abord convertir chaque nombre en chaîne ; on verra la façon élégante de le faire avec les compréhensions (section 6).
Combinés, `split` et `join` permettent de transformer un texte en quelques lignes :
```python
phrase = " python est super "
propre = " ".join(phrase.split())
print(f"[{propre}]") # → [python est super]
```
#### Aligner et remplir
```python
print("chat".center(12, "*")) # → ****chat****
print("chat".ljust(8) + "|") # → chat |
print("chat".rjust(8) + "|") # → chat|
print("7".zfill(3)) # → 007
```
---
### 4. Les f-strings, en plus fin
Tu connais les f-strings depuis le chapitre 01. Après les deux-points, on peut ajouter un **format** très précis :
```python
prix = 1234.5678
print(f"{prix:.2f}") # → 1234.57 (2 décimales)
print(f"{prix:,.2f}") # → 1,234.57 (séparateur de milliers)
print(f"{0.256:.1%}") # → 25.6% (pourcentage)
print(f"{42:05d}") # → 00042 (5 chiffres, complété par des 0)
nom = "Alice"
print(f"[{nom:<10}]") # → [Alice ] (aligné à gauche sur 10)
print(f"[{nom:>10}]") # → [ Alice] (aligné à droite)
print(f"[{nom:^10}]") # → [ Alice ] (centré)
print(f"[{nom:*^11}]") # → [***Alice***] (centré, rempli de *)
```
C'est idéal pour afficher des tableaux bien alignés :
```python
produits = [("Pomme", 0.5, 12), ("Ananas", 2.95, 2), ("Kiwi", 0.35, 30)]
print(f"{'Produit':<10}{'Prix':>8}{'Qté':>6}")
print("-" * 24)
for nom, prix, quantite in produits:
print(f"{nom:<10}{prix:>8.2f}{quantite:>6}")
```
```text
Produit Prix Qté
------------------------
Pomme 0.50 12
Ananas 2.95 2
Kiwi 0.35 30
```
Une autre astuce pratique pour déboguer : `=` après le nom de variable affiche le nom **et** la valeur.
```python
total = 42
print(f"{total=}") # → total=42
```
---
### 5. `enumerate`, `zip` et `sorted`
Ces trois fonctions intégrées rendent les boucles beaucoup plus élégantes.
#### `enumerate` : l'indice et la valeur
Au chapitre 04, pour avoir l'indice, on écrivait `for i in range(len(liste))`. `enumerate` fait mieux : il fournit à chaque tour un tuple `(indice, valeur)`, qu'on déballe.
```python
podium = ["Alice", "Bob", "Chloé"]
for i, nom in enumerate(podium):
print(i, nom)
```
```text
0 Alice
1 Bob
2 Chloé
```
Le paramètre `start` permet de commencer à compter ailleurs qu'à 0 (pratique pour l'affichage humain) :
```python
podium = ["Alice", "Bob", "Chloé"]
for rang, nom in enumerate(podium, start=1):
print(f"{rang}. {nom}")
```
```text
1. Alice
2. Bob
3. Chloé
```
Ça marche aussi sur une chaîne :
```python
for position, lettre in enumerate("abc"):
print(f"La lettre {lettre} est en position {position}")
```
```text
La lettre a est en position 0
La lettre b est en position 1
La lettre c est en position 2
```
#### `zip` : parcourir plusieurs listes en parallèle
`zip` associe les éléments de plusieurs collections **deux à deux** (comme une fermeture éclair, *zip* en anglais).
```python
prenoms = ["Alice", "Bob", "Chloé"]
ages = [30, 25, 35]
villes = ["Lyon", "Paris", "Nice"]
for prenom, age, ville in zip(prenoms, ages, villes):
print(f"{prenom}, {age} ans, habite à {ville}")
```
```text
Alice, 30 ans, habite à Lyon
Bob, 25 ans, habite à Paris
Chloé, 35 ans, habite à Nice
```
Si les listes n'ont pas la même longueur, `zip` s'arrête à la plus courte. Pour transformer deux listes en dictionnaire, `zip` est parfait :
```python
cles = ["nom", "age", "ville"]
valeurs = ["Alice", 30, "Lyon"]
fiche = dict(zip(cles, valeurs))
print(fiche) # → {'nom': 'Alice', 'age': 30, 'ville': 'Lyon'}
```
Et pour comparer des éléments voisins d'une liste, on peut « zipper » la liste avec elle-même décalée :
```python
temperatures = [12, 15, 14, 18, 21]
for avant, apres in zip(temperatures, temperatures[1:]):
print(f"{avant} → {apres} : {apres - avant:+d}")
```
```text
12 → 15 : +3
15 → 14 : -1
14 → 18 : +4
18 → 21 : +3
```
(Le format `+d` affiche toujours le signe.)
#### `sorted` avec `key`
Tu sais déjà que `sorted(liste)` renvoie une copie triée et que `reverse=True` inverse l'ordre. Le paramètre `key` permet de dire **selon quel critère** trier : on lui passe une **fonction**, que `sorted` appelle sur chaque élément ; le tri se fait sur les résultats.
```python
mots = ["kiwi", "Banane", "abricot", "Cerise", "fraise"]
print(sorted(mots)) # → ['Banane', 'Cerise', 'abricot', 'fraise', 'kiwi']
print(sorted(mots, key=str.lower)) # → ['abricot', 'Banane', 'Cerise', 'fraise', 'kiwi']
print(sorted(mots, key=len)) # → ['kiwi', 'Banane', 'Cerise', 'fraise', 'abricot']
```
- Par défaut, les majuscules passent avant les minuscules (`"B" < "a"`).
- `key=str.lower` compare les versions en minuscules : ordre alphabétique « naturel ».
- `key=len` trie par longueur ; à longueur égale, l'ordre d'origine est conservé (le tri est dit **stable**).
On peut passer n'importe quelle fonction, y compris une des tiennes :
```python
def derniere_lettre(mot):
return mot[-1]
mots = ["chat", "souris", "lapin", "zebre"]
print(sorted(mots, key=derniere_lettre)) # → ['zebre', 'lapin', 'souris', 'chat']
```
Les fonctions `min` et `max` acceptent aussi `key` :
```python
mots = ["chat", "hippopotame", "ours"]
print(max(mots, key=len)) # → hippopotame
```
---
### 6. Les fonctions `lambda`
Écrire une fonction complète avec `def` juste pour la passer à `sorted` est un peu lourd. Une **lambda** est une petite fonction **anonyme**, écrite en une seule expression :
```python
# Ces deux écritures sont équivalentes :
def double(x):
return x * 2
double_lambda = lambda x: x * 2
print(double(5)) # → 10
print(double_lambda(5)) # → 10
```
La syntaxe est `lambda parametres: expression`. Pas de `return` : la valeur de l'expression est automatiquement renvoyée. Une lambda ne peut contenir qu'**une seule expression** (pas de `if` sur plusieurs lignes, pas de boucle).
> On ne range pas normalement une lambda dans une variable comme ci-dessus (autant utiliser `def`). Une lambda s'utilise **directement là où on a besoin d'une fonction**, typiquement comme argument `key`.
```python
eleves = [("Alice", 15), ("Bob", 12), ("Chloé", 18), ("David", 12)]
# Trier par note (le 2e élément du tuple)
print(sorted(eleves, key=lambda eleve: eleve[1]))
# Trier par note décroissante
print(sorted(eleves, key=lambda eleve: eleve[1], reverse=True))
# Meilleur élève
print(max(eleves, key=lambda eleve: eleve[1]))
```
```text
[('Bob', 12), ('David', 12), ('Alice', 15), ('Chloé', 18)]
[('Chloé', 18), ('Alice', 15), ('Bob', 12), ('David', 12)]
('Chloé', 18)
```
Avec une liste de dictionnaires :
```python
contacts = [
{"nom": "Durand", "age": 42},
{"nom": "Martin", "age": 25},
{"nom": "Bernard", "age": 33},
]
for contact in sorted(contacts, key=lambda c: c["age"]):
print(contact["nom"], contact["age"])
```
```text
Martin 25
Bernard 33
Durand 42
```
#### Trier sur plusieurs critères
La lambda peut renvoyer un **tuple** : Python trie d'abord selon le premier élément, puis le deuxième en cas d'égalité. Pour inverser un critère numérique, on utilise le signe moins :
```python
eleves = [("Alice", 15), ("Bob", 12), ("Chloé", 18), ("David", 12), ("Emma", 15)]
# Note décroissante, puis nom alphabétique
classement = sorted(eleves, key=lambda e: (-e[1], e[0]))
print(classement)
```
```text
[('Chloé', 18), ('Alice', 15), ('Emma', 15), ('Bob', 12), ('David', 12)]
```
Compare avec l'exercice 8 du chapitre 04 : c'est la même idée, en une ligne.
#### Trier un dictionnaire par valeur
```python
scores = {"Léo": 1200, "Maya": 1850, "Sam": 900}
for nom, score in sorted(scores.items(), key=lambda paire: paire[1], reverse=True):
print(nom, score)
```
```text
Maya 1850
Léo 1200
Sam 900
```
`scores.items()` fournit des tuples `(nom, score)` ; on trie selon `paire[1]`, le score.
---
### 7. Les compréhensions de liste
Tu as souvent écrit ce schéma :
```python
nombres = [1, 2, 3, 4, 5]
carres = []
for n in nombres:
carres.append(n * n)
print(carres) # → [1, 4, 9, 16, 25]
```
Python propose une syntaxe compacte pour exactement ça, la **compréhension de liste** :
```python
nombres = [1, 2, 3, 4, 5]
carres = [n * n for n in nombres]
print(carres) # → [1, 4, 9, 16, 25]
```
Lis-la de gauche à droite : « la liste des `n * n` **pour** chaque `n` **dans** `nombres` ». C'est très proche de la notation mathématique { n² | n ∈ nombres }.
```text
[ n * n for n in nombres ]
───┬── ────────┬───────
│ └─ la boucle : d'où viennent les éléments
└─ l'expression : ce qu'on met dans la nouvelle liste
```
Exemples :
```python
prenoms = ["alice", "bob", "chloé"]
print([p.capitalize() for p in prenoms]) # → ['Alice', 'Bob', 'Chloé']
print([len(p) for p in prenoms]) # → [5, 3, 5]
print([i * 10 for i in range(5)]) # → [0, 10, 20, 30, 40]
nombres = [1, 2, 3]
print("-".join([str(n) for n in nombres])) # → 1-2-3
prix_ht = [10, 25.5, 100]
print([round(p * 1.2, 2) for p in prix_ht]) # → [12.0, 30.6, 120.0]
```
Note l'exemple avec `join` : la compréhension convertit chaque nombre en chaîne, ce qui résout l'erreur vue en section 3.
#### Filtrer avec `if`
On peut ajouter une condition à la fin pour ne garder que certains éléments :
```python
notes = [8, 15, 12, 6, 19, 10]
print([n for n in notes if n >= 10]) # → [15, 12, 19, 10]
mots = ["Python", "est", "un", "langage", "génial"]
print([m for m in mots if len(m) > 3]) # → ['Python', 'langage', 'génial']
print([m.upper() for m in mots if "a" in m]) # → ['LANGAGE', 'GÉNIAL']
```
Correspondance avec la boucle classique :
```python
# Compréhension :
resultat = [m.upper() for m in ["chat", "lion", "rat"] if "a" in m]
# Équivaut exactement à :
resultat2 = []
for m in ["chat", "lion", "rat"]:
if "a" in m:
resultat2.append(m.upper())
print(resultat == resultat2) # → True
```
#### Transformer avec `if/else`
Attention, différence importante : si tu veux **transformer** chaque élément selon une condition (et non filtrer), le `if ... else` se place **avant** le `for`. C'est une expression conditionnelle (vue au chapitre 05).
```python
notes = [8, 15, 12, 6]
print(["admis" if n >= 10 else "recalé" for n in notes]) # → ['recalé', 'admis', 'admis', 'recalé']
```
Pour bien retenir la différence entre les deux formes, voici leur schéma.
```text
[ expression for x in liste if condition ] ← FILTRE : moins d'éléments
[ valeur_si_vrai if condition else valeur_si_faux for x in liste ] ← TRANSFORME : autant d'éléments
```
#### Compréhensions imbriquées
On peut enchaîner plusieurs `for`, dans le même ordre que des boucles imbriquées :
```python
couleurs = ["rouge", "bleu"]
tailles = ["S", "M"]
print([f"{c}-{t}" for c in couleurs for t in tailles])
# → ['rouge-S', 'rouge-M', 'bleu-S', 'bleu-M']
grille = [[1, 2, 3], [4, 5, 6]]
print([valeur for ligne in grille for valeur in ligne]) # aplatir
# → [1, 2, 3, 4, 5, 6]
print([[0] * 3 for _ in range(2)]) # grille 2x3 correcte (cf. piège 8 du chapitre 04)
# → [[0, 0, 0], [0, 0, 0]]
```
```text
['rouge-S', 'rouge-M', 'bleu-S', 'bleu-M']
[1, 2, 3, 4, 5, 6]
[[0, 0, 0], [0, 0, 0]]
```
**Conseil :** une compréhension doit rester **lisible en un coup d'œil**. Si elle dépasse une ligne ou contient plus de deux `for`, une boucle classique est souvent plus claire.
---
### 8. Compréhensions de dictionnaire et d'ensemble
Même principe, avec des accolades.
#### Compréhension de dictionnaire
On écrit `{cle: valeur for ...}` :
```python
mots = ["chat", "éléphant", "rat"]
longueurs = {mot: len(mot) for mot in mots}
print(longueurs) # → {'chat': 4, 'éléphant': 8, 'rat': 3}
prix = {"pain": 1.2, "lait": 0.95, "caviar": 89.0}
print({article: p for article, p in prix.items() if p < 10})
# → {'pain': 1.2, 'lait': 0.95}
print({article: round(p * 2, 2) for article, p in prix.items()})
# → {'pain': 2.4, 'lait': 1.9, 'caviar': 178.0}
# Inverser un dictionnaire (clés ↔ valeurs)
codes = {"FR": "France", "IT": "Italie"}
print({pays: code for code, pays in codes.items()})
# → {'France': 'FR', 'Italie': 'IT'}
```
```text
{'chat': 4, 'éléphant': 8, 'rat': 3}
{'pain': 1.2, 'lait': 0.95}
{'pain': 2.4, 'lait': 1.9, 'caviar': 178.0}
{'France': 'FR', 'Italie': 'IT'}
```
#### Compréhension d'ensemble
`{expression for ...}` sans deux-points crée un ensemble (donc sans doublons) :
```python
mots = ["Chat", "chat", "CHIEN", "chien", "Lapin"]
print(sorted({m.lower() for m in mots})) # → ['chat', 'chien', 'lapin']
phrase = "le chat et le rat"
print(sorted({len(mot) for mot in phrase.split()})) # → [2, 3, 4]
```
#### Et les parenthèses ?
`(x for x in ...)` ne crée **pas** un tuple, mais un **générateur** : un objet qui produit les valeurs une par une, sans tout stocker en mémoire. On l'utilise surtout directement dans une fonction comme `sum`, `max`, `any` ou `all` (les parenthèses de la fonction suffisent) :
```python
nombres = [3, 8, 12, 5]
print(sum(n * n for n in nombres)) # → 242
print(any(n > 10 for n in nombres)) # → True (au moins un ?)
print(all(n > 0 for n in nombres)) # → True (tous ?)
print(max(len(m) for m in ["a", "abc", "ab"])) # → 3
```
`any` renvoie `True` si **au moins un** élément est vrai, `all` si **tous** le sont. Ils remplacent élégamment les boucles avec drapeau du chapitre 05.
---
### 9. Mettre tout ensemble : nettoyer un texte
Voici un exemple réaliste qui combine tout le chapitre : extraire les mots d'un texte, sans ponctuation ni majuscules, et trouver les plus fréquents.
```python
def nettoyer(texte):
"""Renvoie le texte en minuscules, la ponctuation remplacée par des espaces."""
texte = texte.lower()
for signe in ".,;:!?'\"()-":
texte = texte.replace(signe, " ")
return texte
def compter_mots(texte):
"""Renvoie un dictionnaire {mot: nombre d'apparitions}."""
compteur = {}
for mot in nettoyer(texte).split():
compteur[mot] = compteur.get(mot, 0) + 1
return compteur
texte = """Le chat dort. Le chien aboie ! Le chat se réveille,
et le chien, lui, s'endort. C'est la vie du chat."""
compteur = compter_mots(texte)
plus_frequents = sorted(compteur.items(), key=lambda paire: (-paire[1], paire[0]))
for rang, (mot, nombre) in enumerate(plus_frequents[:3], start=1):
print(f"{rang}. {mot:<8} {nombre}")
```
```text
1. le 4
2. chat 3
3. chien 2
```
Remarque le `for rang, (mot, nombre) in enumerate(...)` : `enumerate` fournit `(rang, (mot, nombre))`, et on déballe les deux niveaux d'un coup grâce aux parenthèses.
---
## Pièges fréquents
### Piège 1 — Oublier que les chaînes sont immuables
```python
nom = " alice "
nom.strip()
nom.capitalize()
print(f"[{nom}]") # → [ alice ]
```
Les méthodes ont renvoyé de nouvelles chaînes… qui ont été perdues. **Correction :** récupérer le résultat, éventuellement en enchaînant les méthodes.
```python
nom = " alice "
nom = nom.strip().capitalize()
print(f"[{nom}]") # → [Alice]
```
### Piège 2 — Écrire `join` à l'envers
```python
mots = ["a", "b", "c"]
print(mots.join("-"))
```
```text
Traceback (most recent call last):
File "<stdin>", line 2, in <module>
AttributeError: 'list' object has no attribute 'join'
```
**Correction :** c'est le séparateur qui appelle `join` : `"-".join(mots)`.
### Piège 3 — `join` sur des nombres
```python
print(", ".join([1, 2, 3]))
```
```text
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: sequence item 0: expected str instance, int found
```
**Correction :** convertir d'abord : `", ".join(str(n) for n in [1, 2, 3])`.
### Piège 4 — `split()` contre `split(" ")`
```python
texte = "un deux trois"
print(texte.split(" ")) # → ['un', '', 'deux', '', '', 'trois']
print(texte.split()) # → ['un', 'deux', 'trois']
```
Avec `" "`, chaque espace est un séparateur, ce qui crée des chaînes vides entre deux espaces consécutifs. **Correction :** pour découper en mots, utilise `split()` sans argument.
### Piège 5 — Confondre filtre et transformation dans une compréhension
```python
notes = [8, 15, 12]
print([n for n in notes if n >= 10 else 0])
```
```text
File "<stdin>", line 2
print([n for n in notes if n >= 10 else 0])
^^^^
SyntaxError: invalid syntax
```
Un `if` placé **après** le `for` est un filtre et n'accepte pas de `else`. **Correction :** pour transformer, place le `if/else` **avant** le `for`.
```python
notes = [8, 15, 12]
print([n if n >= 10 else 0 for n in notes]) # → [0, 15, 12]
```
### Piège 6 — Comparer des textes sans normaliser
```python
reponse = "Oui "
if reponse == "oui":
print("Validé")
else:
print("Refusé") # → Refusé
```
L'utilisateur a tapé une majuscule et un espace. **Correction :** `if reponse.strip().lower() == "oui":`.
### Piège 7 — Lambda avec un `return` ou plusieurs instructions
```python
carre = lambda x: return x * x
```
```text
File "<stdin>", line 1
carre = lambda x: return x * x
^^^^^^
SyntaxError: invalid syntax
```
Une lambda contient une **expression**, pas d'instruction : pas de `return`, pas d'affectation, pas de boucle. **Correction :** `lambda x: x * x`, ou une vraie fonction `def` si la logique est plus complexe.
### Piège 8 — Utiliser une variable de boucle après une compréhension
```python
carres = [n * n for n in range(5)]
print(n)
```
```text
Traceback (most recent call last):
File "<stdin>", line 2, in <module>
NameError: name 'n' is not defined
```
La variable d'une compréhension est **locale** à la compréhension, contrairement à celle d'une boucle `for` classique. C'est plutôt une bonne chose : elle n'écrase pas une variable existante.
---
## Exercices
### Exercice 1 — Manipulations de base ⭐
Pour `email = " Jean.Dupont@Exemple.FR "`, écris le code qui :
1. nettoie les espaces et met tout en minuscules ;
2. extrait la partie avant `@` (l'identifiant) et après (le domaine) avec `split` ;
3. extrait le prénom et le nom de l'identifiant (séparés par un point) et les affiche avec une majuscule ;
4. affiche les initiales en majuscules.
Sortie attendue :
```text
Email : jean.dupont@exemple.fr
Identifiant : jean.dupont
Domaine : exemple.fr
Nom complet : Jean Dupont
Initiales : J.D.
```
### Exercice 2 — Compréhensions simples ⭐
Avec des compréhensions (une ligne chacune), à partir de `nombres = [3, -1, 8, 0, -5, 12, 7]` et `mots = ["Soleil", "lune", "Étoile", "ciel", "Nuage"]` :
1. la liste des nombres positifs strictement ;
2. la liste des carrés de tous les nombres ;
3. la liste où chaque négatif est remplacé par 0 ;
4. la liste des mots commençant par une majuscule ;
5. un dictionnaire `{mot en minuscules: longueur}`.
Sortie attendue :
```text
[3, 8, 12, 7]
[9, 1, 64, 0, 25, 144, 49]
[3, 0, 8, 0, 0, 12, 7]
['Soleil', 'Étoile', 'Nuage']
{'soleil': 6, 'lune': 4, 'étoile': 6, 'ciel': 4, 'nuage': 5}
```
### Exercice 3 — Le chiffrement de César ⭐⭐
Écris une fonction `cesar(texte, decalage)` qui décale chaque **lettre minuscule non accentuée** (a-z) de `decalage` positions dans l'alphabet (en revenant au début après `z`) et laisse les autres caractères inchangés. Écris aussi `decoder(texte, decalage)`.
Indice : `alphabet = "abcdefghijklmnopqrstuvwxyz"`, `alphabet.index(lettre)` donne la position, et `% 26` permet de « boucler ».
```python
secret = cesar("rendez-vous a midi !", 3)
print(secret)
print(decoder(secret, 3))
print(cesar("xyz", 2))
```
```text
uhqghc-yrxv d plgl !
rendez-vous a midi !
zab
```
### Exercice 4 — Tableau de notes aligné ⭐⭐
À partir de deux listes, `eleves = ["Alice", "Bob", "Chloé", "Dominique"]` et `notes = [15.5, 9, 12.25, 17]`, affiche un tableau aligné avec un rang (`enumerate`), le nom (aligné à gauche sur 11 caractères), la note (à droite, 2 décimales) et « oui »/« non » pour l'admission (note ≥ 10). Termine par la moyenne.
Sortie attendue :
```text
# Élève Note Admis
1 Alice 15.50 oui
2 Bob 9.00 non
3 Chloé 12.25 oui
4 Dominique 17.00 oui
Moyenne : 13.44
```
### Exercice 5 — Trier des produits ⭐⭐
On donne :
```python
produits = [
{"nom": "Clavier", "prix": 49.9, "stock": 12},
{"nom": "souris", "prix": 19.9, "stock": 0},
{"nom": "Écran", "prix": 189.0, "stock": 4},
{"nom": "casque", "prix": 49.9, "stock": 7},
]
```
Avec `sorted`/`min`/`max` et des lambdas, affiche :
1. les noms triés par prix croissant, puis par nom (insensible à la casse) en cas d'égalité ;
2. le nom du produit le plus cher ;
3. les noms des produits en stock, triés alphabétiquement sans tenir compte de la casse ;
4. la valeur totale du stock (somme de prix × stock), arrondie à 2 décimales.
Sortie attendue :
```text
['souris', 'casque', 'Clavier', 'Écran']
Écran
['casque', 'Clavier', 'Écran']
1704.1
```
### Exercice 6 — Acronymes et titres ⭐⭐
Écris deux fonctions :
- `acronyme(expression)` qui renvoie les initiales en majuscules des mots, en ignorant les petits mots `le, la, les, de, des, du, et, en, un, une` (`"société nationale des chemins de fer"` → `"SNCF"`) ;
- `titre_propre(texte)` qui met une majuscule au début de chaque mot **sauf** pour les petits mots `le, la, les, de, des, du, et, en, un, une` (sauf s'il s'agit du premier mot), et réduit les espaces multiples à un seul.
```python
print(acronyme("société nationale des chemins de fer"))
print(acronyme("organisation des nations unies"))
print(titre_propre(" le seigneur des anneaux et la communauté de l'anneau "))
```
```text
SNCF
ONU
Le Seigneur des Anneaux et la Communauté de L'anneau
```
### Exercice 7 — Anagrammes ⭐⭐⭐
Deux mots sont des anagrammes s'ils contiennent exactement les mêmes lettres (« chien » et « niche »). Écris :
- `signature(mot)` : renvoie les lettres du mot en minuscules, triées, sous forme de chaîne (`"Niche"` → `"cehin"`) ;
- `sont_anagrammes(mot1, mot2)` ;
- `grouper_anagrammes(mots)` : renvoie la liste des groupes (listes) d'au moins 2 mots qui sont anagrammes entre eux, chaque groupe trié, et les groupes triés par leur premier mot.
```python
print(signature("Niche"))
print(sont_anagrammes("chien", "Niche"))
print(sont_anagrammes("chat", "tache"))
mots = ["chien", "niche", "chine", "rage", "gare", "chat", "ironique", "onirique", "Agre"]
print(grouper_anagrammes(mots))
```
```text
cehin
True
False
[['Agre', 'gare', 'rage'], ['chien', 'chine', 'niche'], ['ironique', 'onirique']]
```
### Exercice 8 — Validateur de plaques d'immatriculation ⭐⭐⭐
Une plaque française (système SIV) a le format `AB-123-CD` : 2 lettres majuscules, tiret, 3 chiffres, tiret, 2 lettres majuscules. Écris une fonction `normaliser_plaque(saisie)` qui :
- retire les espaces autour, met en majuscules ;
- accepte aussi les saisies sans tirets (`ab123cd`) ou avec des espaces à la place des tirets (`AB 123 CD`) ;
- renvoie la plaque au format `AB-123-CD` si elle est valide, ou `None` sinon.
Indice : supprime d'abord tirets et espaces, vérifie qu'il reste 7 caractères, puis vérifie chaque morceau avec le slicing, `isalpha()` et `isdigit()`.
```python
for saisie in ["AB-123-CD", "ab123cd", " xy 987 zt ", "A1-234-BC", "AB-12-CD", "ÉÉ-123-AA"]:
print(f"{saisie!r:15} → {normaliser_plaque(saisie)}")
```
```text
'AB-123-CD' → AB-123-CD
'ab123cd' → AB-123-CD
' xy 987 zt ' → XY-987-ZT
'A1-234-BC' → None
'AB-12-CD' → None
'ÉÉ-123-AA' → None
```
(`{saisie!r}` affiche la chaîne avec ses guillemets, ce qui rend les espaces visibles.)
---
## Mini-projet — L'analyseur de texte
### Cahier des charges
Écris un programme qui analyse un texte et produit un rapport. Le texte est stocké dans une variable (au [chapitre 07](07-fichiers-csv-json.md), tu pourras le lire depuis un fichier). Le rapport doit contenir :
1. le nombre de **caractères** (total et hors espaces) ;
2. le nombre de **mots**, de **mots différents** et de **phrases** (une phrase se termine par `.`, `!` ou `?`) ;
3. la **longueur moyenne** des mots ;
4. le **mot le plus long** ;
5. les **5 mots les plus fréquents** parmi les mots d'au moins 4 lettres (pour éviter « le », « de »…), avec une barre de `#` proportionnelle ;
6. les **lettres** les plus fréquentes (top 5), en ignorant les accents de base (é, è, ê → e ; à → a ; ç → c…).
**Contraintes :** structure en fonctions avec docstrings, utilisation de compréhensions, `sorted` avec `key`, `enumerate` ; affichage aligné grâce aux f-strings.
### Étapes suggérées
1. `normaliser(texte)` : minuscules + remplacement des lettres accentuées (un dictionnaire `{"é": "e", ...}` et une boucle de `replace`).
2. `extraire_mots(texte)` : remplace la ponctuation par des espaces puis `split()`. Garde l'apostrophe comme séparateur (« l'analyse » → « l », « analyse »).
3. `compter_phrases(texte)` : compte les `.`, `!` et `?` (attention aux `...` ou `?!` : tu peux d'abord remplacer `...` par `.`, ou compter les morceaux non vides après découpage).
4. `frequences(elements)` : renvoie un dictionnaire de comptage.
5. `top(compteur, n)` : renvoie les `n` paires les plus fréquentes (fréquence décroissante, puis ordre alphabétique).
6. `afficher_rapport(texte)` : assemble le tout.
### Pistes d'amélioration
- Ignorer une liste de **mots vides** (« dans », « pour », « avec »…) au lieu du filtre sur la longueur.
- Calculer un **indice de lisibilité** simple (mots par phrase, lettres par mot).
- Comparer deux textes : mots communs, mots propres à chacun (avec des ensembles).
- Détecter la **langue** (français ou anglais) d'après la fréquence de mots courants (« le », « the »).
- Lire le texte depuis un fichier et écrire le rapport dans un autre ([chapitre 07](07-fichiers-csv-json.md)).
---
## Corrigés
<details>
<summary>Corrigé de l'exercice 1</summary>
```python
email = " Jean.Dupont@Exemple.FR "
email = email.strip().lower()
identifiant, domaine = email.split("@")
prenom, nom = identifiant.split(".")
prenom = prenom.capitalize()
nom = nom.capitalize()
initiales = f"{prenom[0]}.{nom[0]}."
print(f"Email : {email}")
print(f"Identifiant : {identifiant}")
print(f"Domaine : {domaine}")
print(f"Nom complet : {prenom} {nom}")
print(f"Initiales : {initiales}")
```
Sortie :
```text
Email : jean.dupont@exemple.fr
Identifiant : jean.dupont
Domaine : exemple.fr
Nom complet : Jean Dupont
Initiales : J.D.
```
On enchaîne `strip().lower()` : chaque méthode renvoie une nouvelle chaîne sur laquelle on applique la suivante. `split("@")` renvoie une liste de deux éléments qu'on déballe directement. Les initiales s'obtiennent avec `[0]`, le premier caractère.
</details>
<details>
<summary>Corrigé de l'exercice 2</summary>
```python
nombres = [3, -1, 8, 0, -5, 12, 7]
mots = ["Soleil", "lune", "Étoile", "ciel", "Nuage"]
print([n for n in nombres if n > 0])
print([n * n for n in nombres])
print([n if n >= 0 else 0 for n in nombres])
print([m for m in mots if m[0].isupper()])
print({m.lower(): len(m) for m in mots})
```
Sortie :
```text
[3, 8, 12, 7]
[9, 1, 64, 0, 25, 144, 49]
[3, 0, 8, 0, 0, 12, 7]
['Soleil', 'Étoile', 'Nuage']
{'soleil': 6, 'lune': 4, 'étoile': 6, 'ciel': 4, 'nuage': 5}
```
Lignes 1 et 4 : `if` **après** le `for` = filtre (moins d'éléments). Ligne 3 : `if/else` **avant** le `for` = transformation (autant d'éléments). `isupper()` reconnaît aussi les majuscules accentuées comme `É`. On aurait pu écrire la ligne 3 avec `max(n, 0)`.
</details>
<details>
<summary>Corrigé de l'exercice 3</summary>
```python
ALPHABET = "abcdefghijklmnopqrstuvwxyz"
def cesar(texte, decalage):
"""Chiffre texte en décalant chaque lettre a-z de decalage positions."""
resultat = []
for caractere in texte:
if caractere in ALPHABET:
position = ALPHABET.index(caractere)
nouvelle_position = (position + decalage) % 26
resultat.append(ALPHABET[nouvelle_position])
else:
resultat.append(caractere)
return "".join(resultat)
def decoder(texte, decalage):
"""Déchiffre un texte chiffré par cesar avec le même décalage."""
return cesar(texte, -decalage)
secret = cesar("rendez-vous a midi !", 3)
print(secret)
print(decoder(secret, 3))
print(cesar("xyz", 2))
```
Sortie :
```text
uhqghc-yrxv d plgl !
rendez-vous a midi !
zab
```
Le modulo `% 26` fait « boucler » l'alphabet : `x` (23) + 2 = 25 → `z`, `y` (24) + 2 = 26 % 26 = 0 → `a`. Décoder revient à chiffrer avec le décalage opposé ; en Python, `-3 % 26` vaut `23`, donc le modulo fonctionne aussi avec les nombres négatifs. On construit une liste de caractères puis on la `join` : c'est plus efficace que de concaténer des chaînes dans une boucle.
</details>
<details>
<summary>Corrigé de l'exercice 4</summary>
```python
eleves = ["Alice", "Bob", "Chloé", "Dominique"]
notes = [15.5, 9, 12.25, 17]
print(f"{'#':<3}{'Élève':<11}{'Note':>6} Admis")
for rang, (eleve, note) in enumerate(zip(eleves, notes), start=1):
admis = "oui" if note >= 10 else "non"
print(f"{rang:<3}{eleve:<11}{note:>6.2f} {admis}")
moyenne = sum(notes) / len(notes)
print(f"Moyenne : {moyenne:.2f}")
```
Sortie :
```text
# Élève Note Admis
1 Alice 15.50 oui
2 Bob 9.00 non
3 Chloé 12.25 oui
4 Dominique 17.00 oui
Moyenne : 13.44
```
`zip` associe chaque élève à sa note, et `enumerate` ajoute un rang : on déballe `(rang, (eleve, note))`. Les formats `<3`, `<11` et `>6.2f` fixent la largeur des colonnes ; les en-têtes utilisent les mêmes largeurs pour rester alignés. Pour mettre une chaîne littérale dans une f-string délimitée par `"`, on utilise des apostrophes : `{'Élève':<11}`.
</details>
<details>
<summary>Corrigé de l'exercice 5</summary>
```python
produits = [
{"nom": "Clavier", "prix": 49.9, "stock": 12},
{"nom": "souris", "prix": 19.9, "stock": 0},
{"nom": "Écran", "prix": 189.0, "stock": 4},
{"nom": "casque", "prix": 49.9, "stock": 7},
]
# 1. Prix croissant, puis nom insensible à la casse
par_prix = sorted(produits, key=lambda p: (p["prix"], p["nom"].lower()))
print([p["nom"] for p in par_prix])
# 2. Le plus cher
print(max(produits, key=lambda p: p["prix"])["nom"])
# 3. Produits en stock, ordre alphabétique sans casse
en_stock = [p["nom"] for p in produits if p["stock"] > 0]
print(sorted(en_stock, key=str.lower))
# 4. Valeur du stock
valeur = sum(p["prix"] * p["stock"] for p in produits)
print(round(valeur, 2))
```
Sortie :
```text
['souris', 'casque', 'Clavier', 'Écran']
Écran
['casque', 'Clavier', 'Écran']
1704.1
```
La clé de tri `(p["prix"], p["nom"].lower())` est un tuple : à prix égal (49.9), « casque » passe avant « clavier ». `max(..., key=...)` renvoie le dictionnaire entier, dont on prend le nom. Pour la valeur du stock, une expression génératrice dans `sum` évite de créer une liste intermédiaire. Note que « Écran » arrive en dernier car `"é"` est classé après toutes les lettres non accentuées : trier parfaitement du texte français demande des outils plus avancés (module `locale`).
</details>
<details>
<summary>Corrigé de l'exercice 6</summary>
```python
PETITS_MOTS = {"le", "la", "les", "de", "des", "du", "et", "en", "un", "une"}
def acronyme(expression):
"""Renvoie l'acronyme formé des initiales des mots qui ne sont pas des petits mots."""
return "".join(mot[0].upper() for mot in expression.split() if mot.lower() not in PETITS_MOTS)
def titre_propre(texte):
"""Met une majuscule aux mots importants et réduit les espaces."""
mots = texte.lower().split()
resultat = []
for i, mot in enumerate(mots):
if i == 0 or mot not in PETITS_MOTS:
resultat.append(mot.capitalize())
else:
resultat.append(mot)
return " ".join(resultat)
print(acronyme("société nationale des chemins de fer"))
print(acronyme("organisation des nations unies"))
print(titre_propre(" le seigneur des anneaux et la communauté de l'anneau "))
```
Sortie :
```text
SNCF
ONU
Le Seigneur des Anneaux et la Communauté de L'anneau
```
Les petits mots sont rangés dans un **ensemble** partagé par les deux fonctions : le test `in` y est rapide, et la liste n'est écrite qu'une fois. Pour l'acronyme, une expression génératrice filtre les petits mots et prend l'initiale de chacun des autres ; `join` les assemble. Une règle plus simple comme « mots de plus de 2 lettres » ne marcherait pas : « des » fait 3 lettres et donnerait `SNDCF`. Pour `titre_propre`, `split()` sans argument élimine les espaces multiples, et `enumerate` permet de traiter le premier mot à part. `capitalize()` sur « l'anneau » donne « L'anneau » : gérer parfaitement les apostrophes demanderait de découper aussi sur `'`.
</details>
<details>
<summary>Corrigé de l'exercice 7</summary>
```python
def signature(mot):
"""Renvoie les lettres du mot, en minuscules, triées alphabétiquement."""
return "".join(sorted(mot.lower()))
def sont_anagrammes(mot1, mot2):
"""Renvoie True si les deux mots sont des anagrammes."""
return signature(mot1) == signature(mot2)
def grouper_anagrammes(mots):
"""Renvoie les groupes d'au moins deux anagrammes, triés."""
groupes = {}
for mot in mots:
cle = signature(mot)
if cle not in groupes:
groupes[cle] = []
groupes[cle].append(mot)
resultat = [sorted(groupe, key=str.lower) for groupe in groupes.values() if len(groupe) >= 2]
return sorted(resultat, key=lambda groupe: groupe[0].lower())
print(signature("Niche"))
print(sont_anagrammes("chien", "Niche"))
print(sont_anagrammes("chat", "tache"))
mots = ["chien", "niche", "chine", "rage", "gare", "chat", "ironique", "onirique", "Agre"]
print(grouper_anagrammes(mots))
```
Sortie :
```text
cehin
True
False
[['Agre', 'gare', 'rage'], ['chien', 'chine', 'niche'], ['ironique', 'onirique']]
```
L'idée clé : deux anagrammes ont la **même signature** (leurs lettres triées). `sorted("niche")` renvoie une liste de caractères, qu'on rassemble avec `"".join`. Pour grouper, on utilise un dictionnaire `{signature: [mots]}` : tous les anagrammes atterrissent sous la même clé. Une compréhension filtre ensuite les groupes d'au moins deux mots et trie chacun.
</details>
<details>
<summary>Corrigé de l'exercice 8</summary>
```python
def normaliser_plaque(saisie):
"""Renvoie la plaque au format AB-123-CD, ou None si la saisie est invalide."""
brut = saisie.strip().upper().replace("-", "").replace(" ", "")
if len(brut) != 7:
return None
debut, chiffres, fin = brut[:2], brut[2:5], brut[5:]
lettres_ok = all(c in "ABCDEFGHIJKLMNOPQRSTUVWXYZ" for c in debut + fin)
if not lettres_ok or not chiffres.isdigit():
return None
return f"{debut}-{chiffres}-{fin}"
for saisie in ["AB-123-CD", "ab123cd", " xy 987 zt ", "A1-234-BC", "AB-12-CD", "ÉÉ-123-AA"]:
print(f"{saisie!r:15} → {normaliser_plaque(saisie)}")
```
Sortie :
```text
'AB-123-CD' → AB-123-CD
'ab123cd' → AB-123-CD
' xy 987 zt ' → XY-987-ZT
'A1-234-BC' → None
'AB-12-CD' → None
'ÉÉ-123-AA' → None
```
On normalise d'abord (majuscules, sans tirets ni espaces), puis on découpe avec le slicing. `isalpha()` aurait accepté `É` (c'est une lettre !), d'où le test explicite avec la liste des 26 lettres et `all(...)`. Plusieurs `return None` permettent de sortir dès qu'un problème est détecté. Plus tard, tu pourras découvrir les expressions régulières (module `re` de la bibliothèque standard, voir le [chapitre 09](09-modules-bibliotheque-standard.md) pour les modules), conçues pour ce genre de validation.
</details>
<details>
<summary>Corrigé du mini-projet</summary>
```python
ACCENTS = {
"à": "a", "â": "a", "ä": "a",
"é": "e", "è": "e", "ê": "e", "ë": "e",
"î": "i", "ï": "i",
"ô": "o", "ö": "o",
"ù": "u", "û": "u", "ü": "u",
"ç": "c", "œ": "oe", "æ": "ae",
}
PONCTUATION = ".,;:!?'\"()[]«»-–—…’"
def normaliser(texte):
"""Met le texte en minuscules et retire les accents."""
texte = texte.lower()
for accentue, simple in ACCENTS.items():
texte = texte.replace(accentue, simple)
return texte
def extraire_mots(texte):
"""Renvoie la liste des mots du texte, sans ponctuation."""
for signe in PONCTUATION:
texte = texte.replace(signe, " ")
return texte.split()
def compter_phrases(texte):
"""Compte les phrases : morceaux non vides entre deux signes . ! ou ?"""
for signe in "!?":
texte = texte.replace(signe, ".")
morceaux = texte.split(".")
return len([m for m in morceaux if m.strip() != ""])
def frequences(elements):
"""Renvoie un dictionnaire {element: nombre d'apparitions}."""
compteur = {}
for element in elements:
compteur[element] = compteur.get(element, 0) + 1
return compteur
def top(compteur, n):
"""Renvoie les n paires (element, nombre) les plus fréquentes."""
paires = sorted(compteur.items(), key=lambda p: (-p[1], p[0]))
return paires[:n]
def afficher_top(titre, paires):
"""Affiche un classement avec une barre proportionnelle."""
print(f"\n{titre}")
maximum = paires[0][1]
for rang, (element, nombre) in enumerate(paires, start=1):
barre = "#" * round(nombre / maximum * 20)
print(f" {rang}. {element:<12}{nombre:>3} {barre}")
def afficher_rapport(texte):
"""Analyse le texte et affiche un rapport complet."""
mots = extraire_mots(texte.lower())
mots_normalises = extraire_mots(normaliser(texte))
nb_caracteres = len(texte)
nb_sans_espaces = len([c for c in texte if not c.isspace()])
longueur_moyenne = sum(len(m) for m in mots) / len(mots)
plus_long = max(mots, key=len)
print("=" * 40)
print(f"{'RAPPORT D’ANALYSE':^40}")
print("=" * 40)
print(f"Caractères (total) : {nb_caracteres}")
print(f"Caractères (hors espaces): {nb_sans_espaces}")
print(f"Mots : {len(mots)}")
print(f"Mots différents : {len(set(mots))}")
print(f"Phrases : {compter_phrases(texte)}")
print(f"Longueur moyenne des mots: {longueur_moyenne:.2f}")
print(f"Mot le plus long : {plus_long} ({len(plus_long)} lettres)")
mots_longs = [m for m in mots_normalises if len(m) >= 4]
afficher_top("Mots les plus fréquents (4 lettres et +) :", top(frequences(mots_longs), 5))
lettres = [c for c in normaliser(texte) if c.isalpha()]
afficher_top("Lettres les plus fréquentes :", top(frequences(lettres), 5))
texte = """Python est un langage de programmation. Python est apprécié
pour sa lisibilité : un programme Python se lit presque comme de l'anglais !
Les débutants apprécient Python, les experts aussi. Pourquoi ? Parce que
le langage est simple, puissant et polyvalent. Programmer en Python,
c'est programmer avec plaisir."""
afficher_rapport(texte)
```
Sortie :
```text
========================================
RAPPORT D’ANALYSE
========================================
Caractères (total) : 309
Caractères (hors espaces): 262
Mots : 47
Mots différents : 35
Phrases : 6
Longueur moyenne des mots: 5.32
Mot le plus long : programmation (13 lettres)
Mots les plus fréquents (4 lettres et +) :
1. python 5 ####################
2. langage 2 ########
3. programmer 2 ########
4. anglais 1 ####
5. apprecie 1 ####
Lettres les plus fréquentes :
1. e 33 ####################
2. p 22 #############
3. a 21 #############
4. s 19 ############
5. t 19 ############
```
**Points clés de la solution :**
- Chaque fonction a une responsabilité unique : `normaliser`, `extraire_mots`, `frequences` et `top` sont réutilisables pour d'autres analyses (et `frequences` sert deux fois : pour les mots et pour les lettres).
- Les accents sont gérés par un **dictionnaire de correspondances** parcouru avec `items()`.
- On compte les mots sur le texte en minuscules (en gardant les accents pour l'affichage du mot le plus long) et les fréquences sur le texte normalisé.
- `compter_phrases` uniformise les terminaisons, découpe, puis ne garde que les morceaux non vides, ce qui gère les `?!` ou les `...`.
- `top` trie par fréquence décroissante puis ordre alphabétique grâce à la clé `(-p[1], p[0])` : le classement est stable et reproductible.
- La longueur de la barre est proportionnelle au maximum (`nombre / maximum * 20`), quelle que soit la taille du texte.
</details>
---
## Récapitulatif
- Une chaîne est une **séquence immuable** : indices, slicing, `in`, `len` et `for` fonctionnent, mais toute « modification » crée une nouvelle chaîne qu'il faut récupérer.
- Slicing : `s[debut:fin:pas]`, `fin` exclu ; `s[::-1]` inverse ; pas d'erreur en cas de dépassement.
- Méthodes clés : `lower/upper/capitalize/title`, `strip`, `find/count/startswith/endswith`, `replace`, `isdigit/isalpha`, **`split`** (chaîne → liste) et **`join`** (liste → chaîne, appelé sur le séparateur).
- Les f-strings acceptent des formats : `{x:.2f}`, `{nom:<10}`, `{n:>5}`, `{p:.1%}`, `{x=}`.
- `enumerate(liste, start=1)` donne `(indice, valeur)` ; `zip(a, b)` parcourt plusieurs listes en parallèle.
- `sorted(..., key=fonction, reverse=True)` trie selon un critère ; une clé tuple permet plusieurs critères.
- `lambda params: expression` crée une petite fonction anonyme, idéale pour `key`.
- Compréhensions : `[expr for x in seq if cond]` (liste), `{k: v for ...}` (dictionnaire), `{expr for ...}` (ensemble) ; `if` final = filtre, `a if cond else b` en tête = transformation.
- `sum(...)`, `any(...)`, `all(...)`, `max(...)` acceptent une expression génératrice.
### Checklist de validation
- [ ] Je sais extraire n'importe quel morceau d'une chaîne avec le slicing.
- [ ] Je sais nettoyer une saisie avec `strip()` et `lower()` et en récupérer le résultat.
- [ ] Je sais passer d'une chaîne à une liste avec `split` et revenir avec `join`.
- [ ] Je sais aligner un tableau avec les f-strings.
- [ ] J'utilise `enumerate` au lieu de `range(len(...))`.
- [ ] Je sais trier une liste de dictionnaires avec `sorted` et une `lambda`.
- [ ] Je sais écrire une compréhension avec filtre, et une avec transformation `if/else`.
- [ ] J'ai terminé l'analyseur de texte.
---
[← Chapitre précédent](05-fonctions.md) | [Sommaire](README.md) | [Chapitre suivant →](07-fichiers-csv-json.md)