Solutions — série 03-3 (Tuples et dictionnaires)

Rappel de la règle d'or : on n'ouvre ce fichier qu'après avoir terminé (ou sérieusement séché sur) la série. Compare le raisonnement, pas seulement le code : une solution différente qui marche est souvent valable.


Exercice facile a — le tuple ville

Raisonnement — Trois champs figés qui vont ensemble : cas d'école du tuple. Le déballage donne trois noms lisibles ; la tentative de modification est là pour te faire LIRE le TypeError une fois dans ta vie, volontairement.

Solution

# ex-03-3-a.py
city = ("Nice", 43.7, 7.27)

name, lat, lon = city                       # déballage : 3 noms, 3 valeurs
print(f"{name} : lat {lat}, lon {lon}")     # Nice : lat 43.7, lon 7.27

city[0] = "Lyon"
# Traceback (most recent call last):
#   File "...ex-03-3-a.py", line 6, in <module>
#     city[0] = "Lyon"
#     ~~~~^^^
# TypeError: 'tuple' object does not support item assignment
# → "item assignment" = affectation par index ; un tuple est scellé.
#   Pour "changer de ville", on créerait un NOUVEAU tuple.

Pourquoi ça marche — Le déballage exige exactement autant de variables que d'éléments (3 = 3). L'erreur finale est le contrat du tuple : c'est une garantie, pas une limitation — personne ne déplacera Nice par accident.

Erreur classique sur cet exercice — Déballer dans le mauvais ordre (lat, lon, name = city) : AUCUNE erreur, mais lat vaut "Nice" — le déballage est positionnel, pas nominal. Vérifie ta sortie, pas seulement l'absence de traceback.

Variante plus difficile — Crée le « nouveau tuple » proprement : city = ("Lyon",) + city[1:] — pourquoi la virgule dans ("Lyon",) est-elle indispensable ? (⏪ section 4.b : sans elle, + tenterait de coller une str à un tuple → TypeError.)


Exercice facile b — CRUD sur le stock

Raisonnement — L'exercice fait pratiquer les cinq gestes de base dans l'ordre : lire (d[k]), ajouter (d[nouvelle] = v), modifier (d[existante] = v — même syntaxe, c'est la clé qui décide), supprimer (del), mesurer (len).

Solution

# ex-03-3-b.py
stock = {"pommes": 12, "poires": 5, "bananes": 8}

print(stock["poires"])       # 5        — lecture
stock["kiwis"] = 20          # ajout    : clé nouvelle
stock["pommes"] = 10         # modif    : clé existante, valeur écrasée
del stock["bananes"]         # suppression de la paire entière
print(stock)                 # {'pommes': 10, 'poires': 5, 'kiwis': 20}
print(len(stock))            # 3        — len compte les PAIRES

Pourquoi ça marched[k] = v fait deux métiers selon que k existe déjà ou non ; del d[k] retire clé ET valeur ; l'ordre d'affichage suit l'ordre d'insertion (les kiwis, arrivés derniers, sortent derniers).

Erreur classique sur cet exercicestock.append(...) : les dicts n'ont pas d'append (AttributeError) — on n'« ajoute pas au bout » d'un annuaire, on y range sous une étiquette. Et del stock sans crochets : supprime la variable entière, pas une paire.

Variante plus difficile — Remplace del stock["bananes"] par removed = stock.pop("bananes") : quelle est la différence utile ? Et que fait stock.pop("mangues", 0) ? (Comme get : un défaut qui évite le KeyError.)


Exercice facile c — prédire get et in

Raisonnement — Chaque ligne teste une règle précise : get sur clé présente / absente / absente avec défaut, in sur clé présente, et le piège : in sur une valeur.

Solution — Sur d = {"a": 1, "b": 2} :

1          d.get("a")     : clé présente → sa valeur
None       d.get("z")     : clé absente, pas de défaut → None (sans erreur !)
0          d.get("z", 0)  : clé absente → LE défaut fourni
False      "z" in d       : "z" n'est pas une clé
False      2 in d         : in teste les CLÉS — 2 est une VALEUR, pas une clé

Pourquoi ça marchein sur un dict ne regarde que les clés (c'est ce qui le rend rapide). Pour chercher côté valeurs, il faut le dire : 2 in d.values()True.

Erreur classique sur cet exercice — Prédire True à la dernière ligne — piège tendu à dessein. En vrai code, ce malentendu fait « disparaître » des données : on teste if montant in tarifs: en croyant vérifier un prix, alors qu'on vérifie une clé.

Variante plus difficile — Prédis print(d.get("a", 99)) (clé PRÉSENTE avec défaut) et print(d["a"] or 99) avec d = {"a": 0}. (Réponses : 1 — le défaut est ignoré si la clé existe ; 99 — car 0 est falsy, piège du niveau 02 : get avec défaut ne remplace pas un test d'existence par un test de vérité.)


Exercice moyen a — compter les lettres

Raisonnement — Le motif de comptage de l'exemple 5, avec une chaîne comme source : for ch in word parcourt les caractères (⏪ niveau 02). L'affichage trié passe par sorted(counts.items()) : trier des tuples (lettre, compte) trie par lettre d'abord.

Solution

# ex-03-3-d.py
word = "anticonstitutionnellement"

counts = {}
for ch in word:
    counts[ch] = counts.get(ch, 0) + 1

for letter, count in sorted(counts.items()):
    print(f"{letter} : {count}")

Sortie :

a : 1
c : 1
e : 3
i : 3
l : 2
m : 1
n : 5
o : 2
s : 1
t : 5
u : 1

Pourquoi ça marcheget(ch, 0) rend la première rencontre d'une lettre indistinguable des suivantes : « valeur actuelle ou 0 », plus 1 dans tous les cas. sorted(...) sur des items() renvoie une liste de tuples triée par premier élément — que le for déballe.

Erreur classique sur cet exercicecounts[ch] = counts[ch] + 1 sans get : KeyError: 'a' dès la première lettre — c'est exactement le problème que get(ch, 0) résout. Version détournée mais lourde : un if ch in counts: ... else: ... — correct, mais quatre lignes pour une.

Variante plus difficile — Affiche les lettres triées par compte décroissant. Piste honnête : il faut une clé de tri (sorted(counts.items(), key=...)) — le vrai confort arrive avec les fonctions au niveau 4 ; la fiche projet 05 (analyseur de texte) montre la formule prête à l'emploi si tu veux l'utiliser dès maintenant.


Exercice moyen b — le parcours sans .items()

Raisonnementfor name, grade in grades: itère sur les clés du dict. Chaque clé est une chaîne de 2 caractères… et une chaîne de 2 caractères se déballe en 2 variables ! Le programme « marche » donc, en silence, et affiche n'importe quoi.

Solution — Avec {"Bo": 15, "Al": 12} le programme affiche réellement :

B o
A l

Chaque clé ("Bo") est déballée caractère par caractère : name = "B", grade = "o". Aucune erreur — c'est le pire scénario. Avec {"Bob": 15, "Alice": 12} (clés de 3 et 5 caractères), le déballage en 2 variables casse ENFIN :

ValueError: too many values to unpack (expected 2)

La correction, dans les deux cas :

# ex-03-3-e.py — version corrigée
grades = {"Bo": 15, "Al": 12}
for name, grade in grades.items():   # .items() fournit les paires (clé, valeur)
    print(name, grade)
# Bo 15
# Al 12

Pourquoi ça marche.items() produit des tuples (clé, valeur), la seule chose que le for à deux variables est en droit d'attendre. Sans .items(), le for reçoit les clés seules et le déballage s'applique à CHAQUE clé — résultat dépendant de la longueur des chaînes, donc bug intermittent.

Erreur classique sur cet exercice — Conclure « ça marche » après le premier test avec Bo/Al : un bug qui dépend de la longueur des données passe tous les tests courts et explose en production. Moralité (à noter au journal) : deux variables dans un for sur un dict = .items(), systématiquement, même quand « ça a l'air de marcher ».

Variante plus difficile — Prédis ce qu'affiche for name, grade in grades.values(): avec {"Bo": 15}. (Réponse : TypeError: cannot unpack non-iterable int object — on itère sur les valeurs 15, un int ne se déballe pas. Chaque oubli produit SON erreur : apprends à les reconnaître.)


Exercice moyen c — l'annuaire aux tuples

Raisonnement — Structure composée : dict pour retrouver par nom, tuple pour le groupe figé (ville, année). Le parcours déballe DEUX fois : les paires du dict d'abord, le tuple ensuite — soit en deux temps, soit d'un coup avec des parenthèses.

Solution

# ex-03-3-f.py
directory = {
    "Ada": ("Londres", 1815),
    "Linus": ("Helsinki", 1969),
    "Guido": ("Haarlem", 1956),
}

for name, info in directory.items():
    city, birth_year = info                  # déballage du tuple
    print(f"{name} — {city}, {birth_year}")

# variante équivalente, déballage imbriqué en une ligne :
# for name, (city, birth_year) in directory.items():

Sortie :

Ada — Londres, 1815
Linus — Helsinki, 1969
Guido — Haarlem, 1956

Pourquoi ça marche — Chaque valeur du dict est un tuple : info en reçoit un à chaque tour, et se déballe comme n'importe quel tuple. La version imbriquée (city, birth_year) fait la même chose en une étape — utile, mais n'en abuse pas : au-delà de deux niveaux, on ne se relit plus.

Erreur classique sur cet exerciceprint(f"{name} — {info[0]}, {info[1]}") : correct mais illisible — info[0] ne dit pas « ville ». Le déballage EST la documentation. Autre piège : for name, city, birth_year in directory.items(): — trois variables pour des paires, ValueError: not enough values to unpack (expected 3, got 2).

Variante plus difficile — Ajoute l'âge calculé (2026 - birth_year) et n'affiche que les personnes nées avant 1960. Puis inverse : construis by_city, un dict ville → nom — que se passe-t-il si deux personnes habitent la même ville ? (Réponse : l'écrasement silencieux — c'est l'exercice difficile.)


Exercice difficile a — inverser le dict des notes

Raisonnement — Inverser naïvement (inverted[grade] = name) écrase : Ada et Guido ont tous deux 18, le second effacerait le premier. La valeur d'arrivée doit donc être une liste de noms — un « dict de listes ». À chaque paire, deux cas : la note a déjà sa liste (on append), ou pas encore (on la crée avec le premier nom dedans).

Solution

# ex-03-3-g.py
grades = {"Ada": 18, "Linus": 15, "Guido": 18}

by_grade = {}
for name, grade in grades.items():
    if grade in by_grade:
        by_grade[grade].append(name)    # liste existante : on complète
    else:
        by_grade[grade] = [name]        # première fois : on CRÉE la liste
print(by_grade)

Sortie :

{18: ['Ada', 'Guido'], 15: ['Linus']}

Pourquoi ça marcheby_grade[grade] est une liste : append la mute en place, directement DANS le dict — pas besoin de la ressortir puis de la remettre (⏩ c'est la mutabilité, détaillée en 03-4). Le if grade in by_grade distingue « première rencontre » de « rencontre suivante », comme le get(k, 0) du comptage — même problème, autre solution.

Erreur classique sur cet exerciceby_grade[grade] = name (sans liste) : pas d'erreur, mais {18: 'Guido', 15: 'Linus'} — Ada a disparu en silence. L'écrasement de clé est une propriété du dict, pas un bug de Python : c'est TA structure qui devait prévoir la collision.

Variante plus difficile — (1) Même exercice avec by_grade[grade] = by_grade.get(grade, []) + [name] — une seule ligne, sans if : déplie ce qu'elle fait. (2) Trie l'affichage par note décroissante (sorted(by_grade.items(), reverse=True)). (3) ⏩ Le vrai raccourci s'appelle by_grade.setdefault(grade, []).append(name) — garde-le pour plus tard, la version explicite se relit mieux à ton stade.


Mini-projet — « compteur de mots »

Raisonnement — Un pipeline en quatre temps : normaliser (lower() — sinon Le et le divergent), découper (split() → liste), compter (LE motif get(w, 0) + 1), afficher. Deux totaux différents : len(words) compte les mots (liste, doublons inclus), len(counts) compte les mots différents (un dict n'a pas deux fois la même clé).

Solution

# word_counter.py
sentence = input("> ")

words = sentence.lower().split()     # normaliser PUIS découper
counts = {}
for word in words:
    counts[word] = counts.get(word, 0) + 1

# len(words)  : total, doublons comptés (c'est une liste)
# len(counts) : mots DIFFERENTS (les clés d'un dict sont uniques)
print(f"{len(counts)} mots differents, {len(words)} mots au total")
for word, count in counts.items():
    print(f"{word} : {count}")

Exemple d'exécution :

> le chat dort et le chien dort
5 mots differents, 7 mots au total
le : 2
chat : 1
dort : 2
et : 1
chien : 1

Pourquoi ça marchelower() puis split() s'enchaînent : chaque méthode renvoie un nouvel objet sur lequel la suivante s'applique. Le dict fait double emploi à ton avantage : il compte ET dédoublonne — d'où les deux statistiques gratuites. L'ordre d'affichage suit l'ordre de première apparition (insertion des clés).

Erreur classique sur cet exercice — Normaliser APRÈS avoir compté (ou pas du tout) : Le et le deviennent deux entrées, les comptes mentent sans erreur. L'ordre du pipeline n'est pas négociable : normaliser → découper → compter. Autre piège : counts[word] += 1 sans getKeyError au premier mot.

Variante plus difficile — La version complète t'attend dans projects/mini-projects/05-analyseur-de-texte.md : ponctuation nettoyée (strip(".,;:!?")), top 5 par fréquence, fréquence des lettres, longueur moyenne. Tout le nécessaire est dans tes mains depuis cette leçon — vise-la après la 03-4.