Leçon 03-5 — Sets et choisir la bonne structure

1. Objectif

À la fin de cette leçon tu sauras : créer un set, éliminer des doublons en une ligne, tester une appartenance très efficacement, combiner des sets (union, intersection, différence) — et surtout, devant un problème donné, choisir entre liste, tuple, dict et set en justifiant ton choix.

2. Pourquoi c'est important

Le set clôt ta panoplie de structures. Il répond à deux besoins constants : « ai-je déjà vu cette valeur ? » et « quels éléments sont communs / différents entre deux collections ? » — emails dupliqués, mots uniques d'un texte, permissions d'un utilisateur. Mais la vraie compétence de fin de niveau est ailleurs : les données d'un vrai programme se modélisent, et choisir la mauvaise structure se paie en code deux fois plus long et bugué. Cette leçon t'entraîne à faire ce choix vite et bien — tu le referas à chaque programme de ta vie.

3. Explication simple

Un set, c'est un sac de valeurs uniques, sans ordre. Deux règles :

  1. Pas de doublon. Ajouter une valeur déjà présente ne fait rien.
  2. Pas de position. Ni index, ni « premier », ni « dernier » — une valeur est dans le sac, ou pas.
nums = {3, 1, 2, 3, 1}     # accolades, comme un dict SANS les ":"
print(nums)                # {1, 2, 3} — les doublons ont fondu
print(2 in nums)           # True — LE super-pouvoir du set

L'image : le videur d'une salle de concert avec la liste des entrées. Il ne sait pas dans quel ordre vous êtes arrivés, il sait instantanément si tu es déjà entré. C'est exactement ça, un set : une machine à répondre à in.

4. Explication approfondie

a) in : la vraie différence avec la liste. Sur une liste, x in lst parcourt les éléments un par un — sur 1 million d'éléments, jusqu'à 1 million de comparaisons. Sur un set, la réponse est quasi immédiate, quelle que soit la taille (même mécanisme de hachage que les clés de dict — un set, c'est en gros un dict sans valeurs). D'où la règle : beaucoup de tests d'appartenance → convertis en set d'abord (set(lst)).

b) Les mêmes contraintes que les clés de dict. Les éléments d'un set doivent être immuables : int, str, tuple oui ; liste non (TypeError ... unhashable). Et le set lui-même est mutable : add, remove/discard le modifient sur place (⏪ les réflexes de 03-4 s'appliquent : s2 = s1 aliasse, s1.copy() copie).

c) L'affichage ment sur l'ordre. print(s) sort les éléments dans un ordre interne arbitraire — qui peut changer d'une exécution à l'autre pour des chaînes. Ne JAMAIS compter sur cet ordre : pour afficher proprement, sorted(s) (qui renvoie une liste triée).

d) Le piège de la création vide : {} est un dict. Les accolades vides appartiennent historiquement au dict. Le set vide s'écrit set(). C'est LE piège de syntaxe de la leçon (section 6).

e) L'algèbre des sets. Union a | b (dans l'un OU l'autre), intersection a & b (dans les DEUX), différence a - b (dans a mais pas dans b — non symétrique !), différence symétrique a ^ b (dans un seul des deux). Des questions qui demanderaient des doubles boucles s'écrivent en un opérateur.

f) Choisir sa structure — le tableau de décision.

Question à se poserStructureExemple
L'ordre compte ? des doublons possibles ? ça évolue ?listehistorique, file d'attente, scores
Groupe figé de champs qui vont ensemble ?tuplecoordonnées, (nom, âge), clé composée
Retrouver par clé/étiquette ?dictannuaire, comptages, config, JSON
Seule l'appartenance (unique) compte ?setdéjà-vu, tags, doublons, comparaisons

Et les combinaisons sont la norme, pas l'exception : une liste de dicts (les tâches d'une todo-list), un dict de listes (note → prénoms, ⏪ exercice 10-a de 03-3), un dict dont les clés sont des tuples (grille de jeu). Choisir, c'est décrire la donnée à voix haute : « des fiches ordonnées » → liste de dicts ; « qui a déjà voté ? » → set.

Cette leçon est la plus légère du niveau en mécanique — profites-en pour consolider : les études de cas de l'exemple 6 et l'exercice 9-b sont le vrai cœur. Compte 3–4 jours, puis les critères de sortie du README du niveau.

5. Exemples commentés

Exemple 1 — créer, dédoublonner

nums = {3, 1, 2, 3, 1}
print(nums, len(nums))         # {1, 2, 3} 3

emails = ["zoe@x.fr", "ada@x.fr", "zoe@x.fr", "bob@x.fr", "ada@x.fr"]
unique = set(emails)           # set(...) convertit ET dédoublonne
print(len(emails), len(unique))    # 5 3
print(f"{len(emails) - len(unique)} doublon(s) detecte(s)")

Comparer len(lst) et len(set(lst)) : détecteur de doublons en une ligne.

Exemple 2 — modifier : add, remove, discard

tags = {"python", "debutant"}
tags.add("listes")
tags.add("python")             # déjà présent : ne fait RIEN, sans erreur
print(len(tags))               # 3

tags.discard("avance")         # absent : discard se tait...
tags.remove("debutant")        # ...remove, lui, exige la présence
print(sorted(tags))            # ['listes', 'python'] — sorted pour afficher

remove plante sur l'absent (KeyError), discard jamais : choisis selon que l'absence est un bug ou un cas normal (même logique que d[k] vs .get, ⏪ 03-3).

Exemple 3 — l'algèbre : union, intersection, différences

python_course = {"Ada", "Bob", "Chloe", "Dan"}
git_course = {"Chloe", "Dan", "Eve"}

print(python_course | git_course)   # {'Ada', 'Bob', 'Chloe', 'Dan', 'Eve'}
                                    # union : inscrits à AU MOINS un cours
print(python_course & git_course)   # {'Chloe', 'Dan'}
                                    # intersection : inscrits aux DEUX
print(python_course - git_course)   # {'Ada', 'Bob'}
                                    # différence : Python SANS Git
print(git_course - python_course)   # {'Eve'}  ← non symétrique !
print(python_course ^ git_course)   # {'Ada', 'Bob', 'Eve'} : un seul des deux

(L'ordre d'affichage de TES sets peut différer — c'est normal, relis 4.c.)

Exemple 4 — le test d'appartenance à grande échelle

banned_words = {"spam", "pub", "arnaque"}      # un set, car on ne fera
                                               # QUE des tests in
message = "offre speciale sans spam ni arnaque"
for word in message.split():
    if word in banned_words:                   # quasi instantané, même
        print(f"Mot banni : {word}")           # avec 100 000 mots bannis

Sortie :

Mot banni : spam
Mot banni : arnaque

Exemple 5 — compter le vocabulaire unique d'une phrase

sentence = "le chat dort et le chien dort"
words = sentence.split()
unique_words = set(words)
print(len(words), "mots,", len(unique_words), "differents")
# 7 mots, 5 differents
print(sorted(unique_words))
# ['chat', 'chien', 'dort', 'et', 'le']

Combo classique : liste pour compter le total (l'ordre et les doublons comptent), set pour le vocabulaire (seule l'unicité compte), sorted pour afficher.

Exemple 6 — trois mini-études de cas (le tableau de décision en action)

# Cas 1 — "qui a déjà voté ?" : appartenance unique → SET
voters = set()
voters.add("Ada")
print("Ada" in voters)         # True : refuse son second vote

# Cas 2 — "combien de fois chaque mot ?" : mot → compte → DICT
counts = {}
for word in ["le", "chat", "le"]:
    counts[word] = counts.get(word, 0) + 1
print(counts)                  # {'le': 2, 'chat': 1}

# Cas 3 — "les 3 derniers messages, dans l'ordre" : ordre + doublons → LISTE
history = ["salut", "ca va ?", "salut"]
print(history[-2:])            # ['ca va ?', 'salut']

Le set du cas 1 ne peut PAS compter les votes ; le dict du cas 2 le pourrait mais serait surdimensionné pour le cas 1 ; la liste du cas 3 est la seule à garder ordre et doublons. Aucune structure ne domine : c'est le besoin qui décide.

6. Erreurs fréquentes

1) {} pour créer un set vide

seen = {}
print(type(seen))    # <class 'dict'>  — surprise !
seen.add("x")        # AttributeError: 'dict' object has no attribute 'add'

Le set vide, c'est set(). {1, 2} avec du contenu est bien un set, mais les accolades vides font un dict.

2) Ajouter une liste dans un set

seen = set()
seen.add([1, 2])
# TypeError: cannot use 'list' as a set element (unhashable type: 'list')

Éléments immuables uniquement. Pour mémoriser une paire : un tuple — seen.add((1, 2)).

3) Compter sur l'ordre d'un set

podium = {"Ada", "Bob", "Chloe"}
print(podium)        # ordre ARBITRAIRE — peut varier d'une exécution à l'autre

Aucune erreur, mais tout code qui suppose « le premier du set » est faux par construction. Un podium a un ordre → c'était une liste. Pour afficher un set : sorted(podium).

4) Indexer un set

first = podium[0]    # TypeError: 'set' object is not subscriptable

Pas de position, donc pas de crochets. Besoin d'un index → convertis : sorted(podium)[0].

5) remove sur un élément absent

seen = set()
seen.remove(5)       # KeyError: 5

discard(5) si l'absence est un cas normal. (Oui, un KeyError hors d'un dict : sets et dicts partagent leur mécanique interne.)

6) Dédoublonner… en perdant l'ordre dont on avait besoin

history = ["b", "a", "b", "c"]
history = list(set(history))     # doublons partis... ordre parti aussi !

Bug silencieux si l'ordre comptait. Si tu dois garder l'ordre de première apparition : boucle + set des « déjà vus » (c'est l'exercice 10-a).

7. Lire les messages d'erreur

L'élément non hachable :

Traceback (most recent call last):
  File "C:\Users\jtron\Claude\learnpython\dedupe.py", line 2, in <module>
    seen.add([1, 2])
    ~~~~~~~~^^^^^^^^
TypeError: cannot use 'list' as a set element (unhashable type: 'list')

De bas en haut : le message dit tout — une list ne peut pas être élément de set (« unhashable » = non hachable = mutable, donc interdite ici, comme pour les clés de dict). La correction est toujours la même : tuple si la donnée est figée, repenser la structure sinon.

Le KeyError du set :

Traceback (most recent call last):
  File "C:\Users\jtron\Claude\learnpython\dedupe.py", line 2, in <module>
    seen.remove(5)
    ~~~~~~~~~~~^^^
KeyError: 5

Même famille que le KeyError des dicts (⏪ 03-3) : la valeur affichée est l'élément introuvable. Remède : discard, ou un test in avant.

L'indexation interdite :

TypeError: 'set' object is not subscriptable

« subscriptable » = indexable avec [...]. Tu as croisé ce message avec None (⏪ 03-1) et les tuples à un élément ratés (⏪ 03-3) : la dernière ligne d'un traceback te dit toujours le type réel de l'objet — quand ce type te surprend, remonte à la création de la variable.

8. Exercices — faciles

Règles : un fichier par exercice dans lessons/level-03-structures-donnees/exercices/mes-reponses/, nommé ex-03-5-a.py, etc. Prédis chaque sortie AVANT d'exécuter.

a) Avec emails = ["zoe@x.fr", "ada@x.fr", "zoe@x.fr", "bob@x.fr", "ada@x.fr"] : affiche le nombre total d'inscriptions, le nombre d'adresses uniques, le nombre de doublons, puis les adresses uniques triées, une par ligne.

b) Prédis les cinq sorties, puis vérifie :

a = {1, 2, 3, 4}
b = {3, 4, 5}
print(a | b)
print(a & b)
print(a - b)
print(b - a)
print(len(a ^ b))

c) Tape ce code, prédis, exécute, et corrige-le pour qu'il marche :

basket = {}
basket.add("pomme")
print(basket)

(Deux caractères à changer. Explique le piège en commentaire.)

9. Exercices — moyens

a) Deux phrases : p1 = "le chat dort sur le tapis" et p2 = "le chien dort dans le jardin". Affiche : les mots communs aux deux (triés), les mots présents uniquement dans p1 (triés), et le vocabulaire total des deux phrases réunies (son nombre de mots). Uniquement avec split() et des opérations de sets.

b) Choisir et justifier. Pour chacune des cinq situations, écris en commentaire la structure choisie (liste / tuple / dict / set / combinaison) et UNE phrase de justification, puis déclare un exemple plausible en code :

  1. Les pseudos connectés au chat (pas de doublon, on teste qui est là).
  2. Les 10 derniers coups joués d'une partie d'échecs, dans l'ordre.
  3. La fiche d'un produit : nom, prix, stock — retrouvés par champ.
  4. La position (colonne, ligne) d'une pièce sur un plateau.
  5. Le prix de chaque produit d'une boutique, retrouvé par nom de produit.

c) Un questionnaire est rempli deux fois par certains participants. answers = [("Ada", "oui"), ("Bob", "non"), ("Ada", "non")] : détecte les participants en double (avec un set des prénoms déjà vus, en un parcours) et affiche Doublon : Ada. (Les tuples se déballent dans le for — ⏪ 03-3.)

10. Exercices — difficiles

a) Dédoublonner en gardant l'ordre. list(set(lst)) perd l'ordre (⏪ erreur 6). Écris la version qui préserve l'ordre de première apparition : ["b", "a", "b", "c", "a"]["b", "a", "c"]. Un seul parcours ; l'appartenance doit être testée sur un set, pas sur la liste résultat.

b) Les doublons eux-mêmes. À partir de values = [3, 7, 3, 1, 7, 3, 9], construis l'ensemble des valeurs qui apparaissent au moins deux fois : {3, 7} — en un seul parcours, avec deux sets (seen et dupes). Affiche le résultat trié. Pourquoi un set pour dupes (et pas une liste) ? Réponds en commentaire (que se passerait-il avec le 3, présent trois fois ?).

11. Mini-projet lié — « annuaire simple »

Écris directory.py, le troisième mini-projet annoncé du niveau — celui qui combine TOUTES les structures : un menu en boucle pour gérer des contacts.

1. Ajouter  2. Chercher  3. Supprimer  4. Lister  q. Quitter
> 1
Nom : Ada
Ville : Londres
Annee de naissance : 1815
Ada ajoutee.

Contraintes de modélisation (justifie chacune en commentaire — c'est le cœur de l'exercice) : les contacts vivent dans un dict nom → tuple (ville, annee) ; « Ajouter » refuse un nom déjà présent (test in sur le dict) ; « Chercher » utilise .get() et affiche Inconnu au bataillon. si absent ; « Lister » affiche les contacts triés par nom ; le programme maintient aussi un set searched des noms déjà recherchés au cours de la session et l'affiche à la sortie (Recherches de la session : ...). Durée cible : 40–45 min.

Ensuite, deux suites naturelles avec tout ton niveau 03 : projects/mini-projects/06-gestionnaire-de-taches.md (v1 — liste de dicts, mutabilité en action) et l'extension « vocabulaire unique » de projects/mini-projects/05-analyseur-de-texte.md (un set en une ligne). Fais au moins le premier avant de quitter le niveau.

12. Correction / méthode de correction

Les solutions détaillées sont dans solutions/serie-03-5-solutions.md.

Méthode : (1) tous les exercices avant les solutions ; (2) pour 9-b, compare tes justifications, pas seulement tes choix — un bon choix mal justifié est un coup de chance ; (3) journal pour chaque écart ; (4) termine par les critères de sortie du README du niveau : chaque case doit être cochable, la leçon 03-4 re-testée à J+7 incluse.

13. À retenir

14. Questions de révision

  1. Quelles sont les deux propriétés définitoires d'un set, et quelle contrainte pèse sur ses éléments ?
  2. Pourquoi in est-il tellement plus rapide sur un set que sur une liste ?
  3. Comment crée-t-on un set vide, et pourquoi pas avec {} ?
  4. Quelle est la différence entre remove et discard ? Donne le parallèle avec les dicts.
  5. Que renvoient a - b et b - a ? Pourquoi faut-il se méfier ?
  6. Comment détecter en une ligne qu'une liste contient des doublons ?
  7. Pour chaque structure du niveau (liste, tuple, dict, set) : donne la question à se poser et un exemple d'usage typique.
  8. Pourquoi list(set(history)) est-il dangereux si history est un historique ?

15. Checklist de compréhension

Si une case reste vide : refais les exercices de la section correspondante demain. Sinon : niveau 03 terminé — direction les fonctions (niveau 04).

16. Commit conseillé

git add lessons/level-03-structures-donnees/exercices/mes-reponses/
git commit -m "exercises: complete lesson 03-5 sets and data structure choice"

Puis le commit de fin de niveau (voir le README du niveau) :

git add lessons/level-03-structures-donnees
git commit -m "exercises: complete level 03 (data structures)"

Entrée de journal, git push, et note PROGRESS.md.