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 marche — d[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 exercice — stock.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 marche — in 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 marche — get(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 exercice — counts[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()
Raisonnement — for 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 exercice — print(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 marche — by_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 exercice — by_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 marche — lower() 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 get — KeyError 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.