Aller au contenu

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 (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. 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).

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()
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 » :

mot = "Python"
mot[0] = "J"
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 :

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.

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

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
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 :

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. Il fonctionne exactement pareil sur les chaînes : chaine[debut:fin:pas], avec fin exclu.

 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
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.

  | 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 :

mot = "chat"
print(mot[2:100])   # → at
print(mot[10:] == "")   # → True  (on obtient une chaîne vide)

Quelques usages typiques :

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

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 :

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.

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) :

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

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

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 :

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 :

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 :

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))
le chat dort
le-chat-dort
lechatdort
le, chat, dort
le
chat
dort

join n'accepte que des chaînes :

nombres = [1, 2, 3]
print("-".join(nombres))
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 :

phrase = "  python   est    super  "
propre = " ".join(phrase.split())
print(f"[{propre}]")   # → [python est super]

Aligner et remplir

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 :

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 :

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}")
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.

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.

podium = ["Alice", "Bob", "Chloé"]
for i, nom in enumerate(podium):
    print(i, nom)
0 Alice
1 Bob
2 Chloé

Le paramètre start permet de commencer à compter ailleurs qu'à 0 (pratique pour l'affichage humain) :

podium = ["Alice", "Bob", "Chloé"]
for rang, nom in enumerate(podium, start=1):
    print(f"{rang}. {nom}")
1. Alice
2. Bob
3. Chloé

Ça marche aussi sur une chaîne :

for position, lettre in enumerate("abc"):
    print(f"La lettre {lettre} est en position {position}")
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).

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}")
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 :

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 :

temperatures = [12, 15, 14, 18, 21]
for avant, apres in zip(temperatures, temperatures[1:]):
    print(f"{avant} → {apres} : {apres - avant:+d}")
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.

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 :

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 :

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 :

# 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.

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]))
[('Bob', 12), ('David', 12), ('Alice', 15), ('Chloé', 18)]
[('Chloé', 18), ('Alice', 15), ('Bob', 12), ('David', 12)]
('Chloé', 18)

Avec une liste de dictionnaires :

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"])
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 :

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)
[('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

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)
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 :

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 :

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 }.

  [ 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 :

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 :

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 :

# 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).

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.

[ 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 :

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]]
['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 ...} :

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'}
{'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) :

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) :

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.

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}")
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

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.

nom = "  alice  "
nom = nom.strip().capitalize()
print(f"[{nom}]")   # → [Alice]

Piège 2 — Écrire join à l'envers

mots = ["a", "b", "c"]
print(mots.join("-"))
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

print(", ".join([1, 2, 3]))
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(" ")

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

notes = [8, 15, 12]
print([n for n in notes if n >= 10 else 0])
  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.

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

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

carre = lambda x: return x * x
  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

carres = [n * n for n in range(5)]
print(n)
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 :

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 :

[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 ».

secret = cesar("rendez-vous a midi !", 3)
print(secret)
print(decoder(secret, 3))
print(cesar("xyz", 2))
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 :

#  É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 :

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 :

['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.

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 "))
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.

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))
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().

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)}")
'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, 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).

Corrigés

Corrigé de l'exercice 1
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 :
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
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 :
[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
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 :
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
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 :
#  É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
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 :
['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
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 :
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
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 :
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
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 :
'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
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 :
========================================
           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 | Sommaire | Chapitre suivant →