# 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 "", line 2, in 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 "", line 2, in 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 "", line 2, in 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 "", line 1, in 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 "", 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 "", 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 "", line 2, in 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
Corrigé de l'exercice 1 ```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.
Corrigé de l'exercice 2 ```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)`.
Corrigé de l'exercice 3 ```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.
Corrigé de l'exercice 4 ```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}`.
Corrigé de l'exercice 5 ```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`).
Corrigé de l'exercice 6 ```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 `'`.
Corrigé de l'exercice 7 ```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.
Corrigé de l'exercice 8 ```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.
Corrigé du mini-projet ```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.
--- ## 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)