Listes, dictionnaires, ensembles, boucles et compréhensions
Quand utiliser chacun des quatre conteneurs de Python, boucles et conditions, compréhensions, et comptage de mots dans des tickets désordonnés pour obtenir un top cinq identique à chaque fois.
Comment voulez-vous apprendre ?
Le problème que nous résolvons
Le responsable du support veut savoir de quoi les clients se sont plaints le plus dans les tickets de cette semaine. Les tickets sont du texte désordonné : certains en majuscules, d'autres bourrés de ponctuation, d'autres qui répètent deux fois la même chose. Un collègue a écrit un script en cinq minutes et envoyé une liste : the en tête, puis refund, puis Refund et refund. comme entrées séparées.
Le second lancement a posé un autre problème : un petit changement dans l'ordre des tickets a modifié le cinquième mot du top 5, alors que le compte d'aucun mot n'avait changé. Personne, en lisant le rapport, ne pouvait dire quelle version était la bonne.
La solution n'est pas une bibliothèque spéciale. Ce sont les quatre conteneurs de base de Python — listes, tuples, dictionnaires et ensembles — ainsi que les boucles et les conditions utilisées correctement. Une grande part du travail quotidien d'un ingénieur en IA consiste exactement à cela : nettoyer du texte, compter, filtrer et trier, de façon à obtenir le même résultat à chaque fois pour une même entrée.
À la fin de ce chapitre vous pourrez
- Choisir entre une liste, un tuple, un dictionnaire et un ensemble pour une tâche, et dire pourquoi
- Écrire des boucles propres avec
for,while,if/elif/else,enumerateet.items() - Filtrer et transformer des données avec des compréhensions de liste, de dictionnaire et d'ensemble
- Extraire les mots d'un texte désordonné et les compter avec un dictionnaire, puis avec
Counter - Trier les mots à égalité de compte selon une règle explicite, pour que le résultat soit toujours le même
Ce qu'il vous faut d'abord : pathlib, configuration JSON/YAML et variables d'environnement — et savoir lancer un script avec uv run.
Quatre conteneurs, quatre usages
Commençons par les quatre, côte à côte :
tickets = ["refund", "login", "refund", "billing"]
latest = ("T-1042", "refund", 3)
owners = {"refund": "Maya", "login": "Omar"}
seen = set(tickets)
print(tickets[0], tickets[-1], len(tickets))
print(latest[1])
print(owners["login"])
print(sorted(seen))
print("login" in seen, "shipping" in seen)refund billing 4
refund
Omar
['billing', 'login', 'refund']
True FalseChacun fait une promesse différente, et vous choisissez selon la promesse :
- Liste (
[...]) — garde l'ordre, accepte la même valeur plusieurs fois, et peut être modifiée. Pour « une suite de choses qui se succèdent » : une liste de tickets, un lot de prompts. - Tuple (
(...)) — garde l'ordre, mais ne peut plus être modifié une fois créé. Pour quelques valeurs qui forment ensemble un seul enregistrement, comme("T-1042", "refund", 3)— identifiant, catégorie, priorité. Ici, la position porte le sens. - Dictionnaire (
{clé: valeur}) — retrouve une valeur à partir de sa clé. « Quel responsable pour quelle catégorie », « combien de fois apparaît chaque mot » — partout où l'on passe d'une chose à une autre. - Ensemble (
set(...)ou{a, b}) — chaque valeur n'apparaît qu'une fois, sans ordre garanti. Pour les cas où la seule question est « est-ce là ? » ou « combien de valeurs distinctes ? ».
sorted(seen) est affiché parce que l'ordre d'un ensemble affiché n'est pas garanti : il peut apparaître dans un ordre à une exécution et dans un autre à la suivante. Si la sortie d'un programme dépend de l'ordre d'un ensemble, cette sortie ne sera pas stable non plus.
insur une liste vérifie chaque élément un par un ; sur un ensemble ou un dictionnaire, la réponse arrive presque immédiatement, qu'il y ait dix éléments ou dix millions. Si vous écrivezif word in stop_wordsencore et encore,stop_wordsdoit être un ensemble, pas une liste. Le pourquoi, et la façon de mesurer la différence, viendront plus tard dans le cours.
Boucles : dites ce que vous parcourez
Les boucles sur des tuples et des dictionnaires se lisent plus clairement avec le unpacking : les valeurs vont directement dans des variables nommées :
ticket_id, category, priority = ("T-1042", "refund", 3)
print(ticket_id, category, priority)
owners = {"refund": "Maya", "login": "Omar"}
for category, owner in owners.items():
print(f"{category} -> {owner}")
for position, category in enumerate(["refund", "login"], start=1):
print(position, category)T-1042 refund 3
refund -> Maya
login -> Omar
1 refund
2 login.items() donne une paire (clé, valeur) à chaque tour, et enumerate donne chaque élément accompagné de sa position — plus besoin de compter les indices avec range(len(...)). Depuis Python 3.7, un dictionnaire retient l'ordre dans lequel ses clés ont été insérées ; la boucle sur owners commence donc par refund.
Que se passe-t-il si vous oubliez .items() ?
owners = {"refund": "Maya", "login": "Omar"}
for category, owner in owners:
print(category, owner)Traceback (most recent call last):
File "main.py", line 2, in <module>
for category, owner in owners:
^^^^^^^^^^^^^^^
ValueError: too many values to unpack (expected 2)Boucler directement sur un dictionnaire ne donne que les clés. La première clé, "refund", est une chaîne de six caractères, et Python essaie de la découper en deux variables — six caractères, deux emplacements. Le message paraît étrange tant qu'on ne se souvient pas qu'une chaîne est aussi une séquence.
if/elif/else s'arrête à la première condition vraie, donc l'ordre des conditions fait toute la logique :
latencies_ms = [120, 340, 95, 1800, 210, 2600]
for latency in latencies_ms:
if latency >= 2000:
label = "critical"
elif latency >= 1000:
label = "slow"
else:
label = "ok"
print(latency, label)120 ok
340 ok
95 ok
1800 slow
210 ok
2600 criticalÉcrivez les conditions dans l'ordre inverse —>= 1000d'abord,>= 2000ensuite — et il n'y a aucune erreur, mais2600devientslow, car la première condition est vraie et la chaîne s'arrête là. La branchecriticalne s'exécute jamais. Quand vous découpez selon des seuils, placez la condition la plus stricte en premier.
for parcourt une collection connue. while continue tant qu'une condition est vraie — quand on ne sait pas à l'avance combien de tours il faudra. Récupérer des données d'une API page par page est le cas classique. Ici, un dictionnaire joue le rôle de l'API :
pages = {1: ["t1", "t2"], 2: ["t3"], 3: []}
page = 1
collected = []
while True:
batch = pages[page]
if not batch:
break
collected.extend(batch)
page += 1
print(collected, "pages read:", page - 1)['t1', 't2', 't3'] pages read: 2Une page vide déclenche break. if not batch fonctionne parce qu'une liste vide est fausse. Dans ce genre de boucle, écrivez d'abord la condition d'arrêt, le reste ensuite — un while True qui oublie de s'arrêter tourne indéfiniment, sans aucune erreur.
Compréhensions : une collection à partir d'une autre
« Une liste vide, une boucle, un if, un append » — ce schéma de quatre lignes est si courant que Python propose une forme en une seule ligne :
latencies_ms = [120, 340, 95, 1800, 210, 2600]
slow = [ms for ms in latencies_ms if ms >= 1000]
seconds = [ms / 1000 for ms in latencies_ms]
labels = {ms: ("slow" if ms >= 1000 else "ok") for ms in latencies_ms}
buckets = {ms // 1000 for ms in latencies_ms}
print(slow)
print(seconds)
print(labels)
print(sorted(buckets))[1800, 2600]
[0.12, 0.34, 0.095, 1.8, 0.21, 2.6]
{120: 'ok', 340: 'ok', 95: 'ok', 1800: 'slow', 210: 'ok', 2600: 'slow'}
[0, 1, 2]Mode d'emploi : [résultat for élément in collection if condition]. La partie gauche dit ce qu'il faut construire à partir de chaque élément ; le if final dit lesquels garder. La même forme avec {clé: valeur for ...} construit un dictionnaire, et {valeur for ...} un ensemble — buckets contient trois tranches d'une seconde distinctes, chacune une seule fois.
Ne confondez pas les deux sortes de if. Le if ms >= 1000 final est un filtre — quand il est faux, l'élément est écarté. "slow" if ms >= 1000 else "ok" est un choix — chaque élément reste, et sa valeur est l'une des deux. C'est pourquoi labels contient les six.
Une compréhension sert à construire une nouvelle collection. Quand vous voulez seulement afficher quelque chose ou écrire dans un fichier — quand le résultat serait jeté —, écrivez une boucle for ordinaire. Et une compréhension imbriquée sur trois niveaux peut tenir sur une ligne, mais une boucle de cinq lignes est souvent plus facile à lire pour un collègue.
Compter les mots : à la main, puis avec Counter
Presque tout le monde tente d'abord de compter avec un dictionnaire de cette façon :
words = ["refund", "late", "refund", "login", "refund", "late"]
counts = {}
for word in words:
counts[word] += 1Traceback (most recent call last):
File "main.py", line 5, in <module>
counts[word] += 1
~~~~~~^^^^^^
KeyError: 'refund'counts[word] += 1 signifie counts[word] = counts[word] + 1 — et la lecture de counts[word] à droite échoue, car la clé "refund" n'existe pas encore. Un mot vu pour la première fois a besoin d'une valeur de départ. .get(clé, 0) fournit exactement cela :
words = ["refund", "late", "refund", "login", "refund", "late"]
counts = {}
for word in words:
counts[word] = counts.get(word, 0) + 1
print(counts){'refund': 3, 'late': 2, 'login': 1}Ce besoin est assez courant pour que la bibliothèque standard propose un type dédié — collections.Counter, une sorte de dictionnaire :
from collections import Counter
words = ["refund", "late", "login", "refund", "login", "late", "slow"]
counts = Counter(words)
print(counts)
print(counts.most_common(2))
print(counts["missing"])Counter({'refund': 2, 'late': 2, 'login': 2, 'slow': 1})
[('refund', 2), ('late', 2)]
0Lire une clé absente dans un Counter donne 0, et non une KeyError — exactement ce qu'il faut pour compter. Mais regardez most_common(2) : refund, late et login ont tous un compte de 2, et ce sont les deux premiers qui sont ressortis. Pourquoi login a-t-il été écarté ? Parce qu'à égalité de compte, most_common garde l'ordre dans lequel les mots ont été vus pour la première fois. login est apparu en troisième dans la liste, il a donc perdu. Changez l'ordre des tickets et le résultat change aussi — c'est le mystère du début du chapitre.
Nettoyer le texte : quels mots sont vraiment des mots ?
split() ne coupe que sur les espaces :
text = "Refund not received. REFUND please! Login fails; login page is slow."
print(text.split())
print(text.lower().split())['Refund', 'not', 'received.', 'REFUND', 'please!', 'Login', 'fails;', 'login', 'page', 'is', 'slow.']
['refund', 'not', 'received.', 'refund', 'please!', 'login', 'fails;', 'login', 'page', 'is', 'slow.'].lower() a réglé la différence entre majuscules et minuscules, mais received., please! et slow. gardent leur ponctuation — et, lors du comptage, slow. et slow sont deux mots différents. C'est là qu'une expression régulière aide : au lieu de dire ce qu'il faut jeter, vous dites à quoi ressemble un mot :
import re
text = "Refund not received. REFUND please! Login fails; login page is slow. Can't pay."
words = re.findall(r"[a-z']+", text.lower())
print(words)['refund', 'not', 'received', 'refund', 'please', 'login', 'fails', 'login', 'page', 'is', 'slow', "can't", 'pay'][a-z']+ signifie « une ou plusieurs lettres anglaises minuscules ou apostrophes ». re.findall renvoie chaque endroit du texte où le motif correspond, et tout le reste — points, virgules, chiffres — est écarté. can't reste un seul mot.
Ce motif a une faille : si quelqu'un écrit'invalid token', le premier mot sort sous la forme'invalid— avec le guillemet attaché. L'exemple suivant resserre le motif pour qu'une apostrophe ne puisse se trouver qu'entre deux lettres. Et rappelez-vous que[a-z]ne couvre que les lettres anglaises ; un texte dans d'autres écritures demande un autre motif.
Égalité de compte : décidez vous-même de la règle de tri
Pour transformer des comptes en classement, il faut trier. Trois façons, sur un dictionnaire où refund et login ont tous deux un compte de 3 :
counts = {"refund": 3, "late": 2, "login": 3, "slow": 1}
print(sorted(counts, reverse=True))
print(sorted(counts.items(), key=lambda item: item[1], reverse=True))
print(sorted(counts.items(), key=lambda item: (-item[1], item[0])))['slow', 'refund', 'login', 'late']
[('refund', 3), ('login', 3), ('late', 2), ('slow', 1)]
[('login', 3), ('refund', 3), ('late', 2), ('slow', 1)]La première ligne est la plus dangereuse, parce qu'elle semble raisonnable. Trier un dictionnaire trie ses clés, pas ses valeurs — on obtient donc l'ordre alphabétique inverse, sans aucun rapport avec les comptes. slow n'est apparu qu'une fois, pourtant il est en tête. Aucune erreur.
La deuxième ligne trie par compte. Mais lequel vient en premier, refund ou login ? Le sorted de Python est « stable » — les éléments de même clé gardent l'ordre qu'ils avaient à l'entrée. refund a été inséré en premier dans le dictionnaire, il passe donc devant. Autrement dit, le résultat dépend de l'ordre dans lequel les données sont arrivées.
La troisième ligne énonce la règle elle-même. key renvoie un tuple, et les tuples se comparent élément par élément : d'abord -item[1] — le compte négatif, pour que les plus grands comptes viennent en premier — puis, à égalité, item[0], le mot lui-même, par ordre alphabétique. Désormais le résultat est le même quel que soit l'ordre d'arrivée des données. Pas besoin de reverse=True, car le signe moins a déjà inversé l'ordre des comptes tout en laissant l'ordre alphabétique tel quel.
Partout où des résultats seront comparés (rapports, tests, évaluations), donnez une règle explicite pour ordonner les valeurs égales. Sinon, le rapport d'aujourd'hui et celui de demain différeront là où les données, elles, n'ont pas changé.
Un exemple complet
Rassemblons maintenant tout cela : les cinq premiers mots des tickets. Les mots vides — des mots comme the et and qui apparaissent dans presque tous les textes sans rien apporter — sont écartés, tout comme les mots de deux lettres, et les égalités sont départagées par ordre alphabétique. top_words.py :
import re
from collections import Counter
STOP_WORDS = {
"a", "and", "for", "i", "is", "it", "my", "not", "of", "on", "the", "to", "was", "with",
}
TICKETS = [
"Refund not received for order 1182. I was charged twice!",
"The app is slow and the login page freezes.",
"Charged twice again. Please refund the second charge.",
"Login fails with 'invalid token' on my phone.",
"Still waiting on my refund, it was promised last week.",
"App slow after the update; login takes a minute.",
]
def tokenize(text: str) -> list[str]:
return re.findall(r"[a-z]+(?:'[a-z]+)?", text.lower())
def top_words(texts: list[str], n: int = 5) -> list[tuple[str, int]]:
counts = Counter(
word
for text in texts
for word in tokenize(text)
if word not in STOP_WORDS and len(word) > 2
)
ranked = sorted(counts.items(), key=lambda item: (-item[1], item[0]))
return ranked[:n]
def print_ranking(ranking: list[tuple[str, int]]) -> None:
if not ranking:
print("no words to rank")
return
width = max(len(word) for word, _ in ranking)
for rank, (word, count) in enumerate(ranking, start=1):
print(f"{rank}. {word:<{width}} {count} {'#' * count}")
if __name__ == "__main__":
print_ranking(top_words(TICKETS))
print_ranking(top_words([]))uv run top_words.py1. login 3 ###
2. refund 3 ###
3. app 2 ##
4. charged 2 ##
5. slow 2 ##
no words to rankLes décisions derrière ce code :
Le motif de mots [a-z]+(?:'[a-z]+)?. Une ou plusieurs lettres, puis éventuellement une apostrophe et d'autres lettres. can't reste un seul mot, mais le guillemet autour de 'invalid token' ne fait plus partie d'un mot. (?:...) est un groupe non capturant — avec un simple (...), re.findall ne renverrait que la partie du groupe au lieu du mot entier.
Les mots vides sont un ensemble. Chaque mot est vérifié avec word not in STOP_WORDS ; sur un ensemble, ce test est quasi instantané, quelle que soit la taille de la liste.
Counter alimenté directement par un générateur. Deux for et un if forment un seul flux — chaque mot de chaque ticket qui passe le filtre. Aucune grande liste intermédiaire de tous les mots n'est construite. Les deux for suivent le même ordre que des boucles imbriquées classiques : le plus externe en premier.
Un départage explicite. login et refund ont tous deux 3 ; app, charged, slow et twice ont tous deux 2. L'ordre alphabétique a décidé qui prenait la cinquième place, et twice a donc été écarté. La règle n'est pas parfaite — twice n'a pas été écarté parce qu'il serait moins important — mais elle est déclarée, et elle est la même à chaque fois. Si quelqu'un qui lit le rapport pose la question, il existe une réponse.
Entrée vide gérée volontairement. max() sur une collection vide lève ValueError: max() iterable argument is empty. Une semaine calme sans ticket est normale, pas une erreur — print_ranking vérifie donc d'abord si le classement est vide. La deuxième ligne de sortie le montre.
Calcul et affichage séparés. top_words renvoie une liste et n'affiche rien ; print_ranking ne fait qu'afficher. Ainsi, les mêmes comptes peuvent servir depuis un test, une API ou un tableau de bord. {word:<{width}} aligne chaque mot à gauche dans un espace de la largeur du plus long, pour que la colonne des comptes soit alignée.
Limite : charged et charge sont ici deux mots distincts. Regrouper les formes d'un même mot est un problème à part, et les systèmes d'IA font généralement correspondre par le sens plutôt qu'en comptant des mots — une approche qui viendra plus tard dans le cours. Mais même là, nettoyer le texte, filtrer et trier dans un ordre stable — le travail de ce chapitre — restent les premières étapes.
Quand ça casse
KeyError: 'refund' counts[word] += 1 pour une clé qui ne figure pas encore dans le dictionnaire. Écrivez counts[word] = counts.get(word, 0) + 1, ou utilisez collections.Counter.
ValueError: too many values to unpack (expected 2) Le code écrit for key, value in d: sur un dictionnaire. Boucler directement sur un dictionnaire ne donne que les clés ; écrivez d.items() pour obtenir des paires.
TypeError: unhashable type: 'list' Une liste a été placée dans un ensemble ou utilisée comme clé de dictionnaire. Les éléments d'un ensemble et les clés d'un dictionnaire doivent être des valeurs qui ne peuvent pas changer. Utilisez un tuple à la place de la liste : seen.add(("refund", "late")).
RuntimeError: dictionary changed size during iteration Des clés ont été supprimées ou ajoutées dans un dictionnaire pendant qu'on le parcourait. Construisez un nouveau dictionnaire au lieu de supprimer : counts = {word: n for word, n in counts.items() if n >= 2}.
AttributeError: 'tuple' object has no attribute 'append' Un tuple ne peut pas être modifié après sa création. Si les valeurs grandissent et rétrécissent vraiment, ce devait être une liste ; si le tuple est un enregistrement, construisez un nouveau tuple avec la nouvelle valeur.
ValueError: max() iterable argument is empty max() ou min() sur une collection vide. Vérifiez d'abord qu'elle n'est pas vide, ou donnez une valeur par défaut : max(lengths, default=0).
Étape 4 sur 6 — Prédire
Vérifiez votre compréhension
Un collègue prend « le mot le plus fréquent » à partir d'un dictionnaire de comptes. Qu'est-ce qui s'affiche ?
counts = {"refund": 5, "slow": 1, "login": 3}
print(sorted(counts, reverse=True)[0])- A`refund`
- B`('refund', 5)`
- C`slow`
- D`5`
label(2600) devrait renvoyer "critical", mais il renvoie "slow" — sans aucune erreur. Où est le bug ?
def label(latency_ms):
if latency_ms >= 1000:
return "slow"
elif latency_ms >= 2000:
return "critical"
return "ok"
print(label(2600))- AL'ordre des conditions : `>= 1000` est vraie en premier et la chaîne s'arrête, donc `>= 2000` n'est jamais atteinte — la condition la plus stricte doit venir en premier
- B`return "ok"` il manque un `else:` juste avant
- CLa deuxième condition doit utiliser `if` au lieu de `elif`
- D`2600` est un entier, il faut donc le convertir avec `float()` avant de le comparer avec `>=`
Un pipeline vérifie, pour chacun d'un million de mots, s'il fait partie des 300 mots vides. Quel conteneur doit contenir les mots vides ?
- AUne chaîne, avec tous les mots réunis en un seul long texte
- BUn tuple, car les mots vides ne changent pas
- CUn ensemble, car un test `in` est quasi instantané quelle que soit sa taille
- DUne liste, car elle peut être gardée dans l'ordre alphabétique
Jouer fait partie de Pro
+30 XP vous attendent
Jouez ce chapitre avec Pro
La lecture est gratuite et tout le chapitre est ouvert. Les 5 premiers chapitres de chaque cours gratuit se jouent gratuitement. À partir du chapitre 6, répondre aux questions, gagner des XP et faire grandir votre série fait partie de Pro.
Débloquer le jeu avec ProÀ vous de jouer
Maintenant, trouvez les mots les plus fréquents par catégorie. Dans un fichier nommé categories.py, copiez le STOP_WORDS et le tokenize du chapitre, et ajoutez ces tickets :
TICKETS = [
("billing", "Charged twice for one order."),
("billing", "Refund for the double charge, please."),
("login", "Login fails on the new phone."),
("billing", "Still no refund after the charge."),
("login", "Password reset email never arrives."),
("login", "Login page shows a blank screen."),
]Écrivez ensuite un code qui :
- Donne à chaque catégorie ses propres comptes — un dictionnaire dont les clés sont les noms de catégories et dont les valeurs sont le
Counterde cette catégorie (indice :dict.setdefaultetCounter.update) - Ne compte pas les mots vides, ni les mots de deux lettres ou moins
- Affiche pour chaque catégorie ses deux mots les plus fréquents, les comptes les plus élevés en premier et l'ordre alphabétique en cas d'égalité
- Affiche les catégories par ordre alphabétique, quel que soit l'ordre d'arrivée des tickets
uv run categories.py doit afficher exactement ceci :
billing charge=2, refund=2
login login=2, arrives=1Inversez ensuite la liste TICKETS (TICKETS[::-1]) et relancez le programme — la sortie change-t-elle ? Si oui, quelle condition n'est pas respectée ? Enfin, supprimez le départage et triez uniquement par compte : quel est le deuxième mot pour login, et reste-t-il le même quand vous inversez l'ordre ?
Étape 6 sur 6
Défi — le quiz du chapitre
Dix questions, de la plus facile à la plus difficile. Les dernières sont difficiles exprès.
Jouer avec Pro