Chapitre 619 minDébutant
Dictionnaires, ensembles et compréhensions
Par Josselyn C., prof de BTS SIO. Mis à jour le .
À la fin de ce chapitre, tu sauras
- créer, lire et parcourir un dictionnaire ;
- compter et regrouper avec Counter et defaultdict ;
- comparer deux listes avec les opérations d'ensembles ;
- écrire des compréhensions lisibles et choisir la bonne structure.
3 exercices corrigés1 schéma
Un dictionnaire (dict) associe des clés à des valeurs, comme un annuaire associe un nom à un numéro ; un ensemble (set) est une collection sans doublon et sans ordre ; une compréhension construit une liste, un dictionnaire ou un ensemble en une seule ligne. Ce chapitre montre comment créer et parcourir un dictionnaire, compter et regrouper avec Counter et defaultdict, manipuler une liste de dictionnaires, comparer deux listes avec les ensembles, puis écrire des compréhensions lisibles, et comment choisir la bonne structure.
Le dictionnaire
Un dictionnaire s'écrit entre accolades, avec des paires clé: valeur. On accède à une valeur par sa clé, et non par une position :
>>> ips = {"srv-ad": "192.168.10.177", "srv-ad2": "192.168.10.181"}
>>> ips["srv-ad"]
'192.168.10.177'
>>> ips["srv-nas"] = "192.168.10.183"
>>> ips["srv-ad2"] = "192.168.10.181"
>>> len(ips)
3
>>> "srv-nas" in ips
True
>>> ips.get("srv-glpi")
>>> ips.get("srv-glpi", "inconnue")
'inconnue'
>>> ips["srv-glpi"]
Traceback (most recent call last):
File "<python-input-8>", line 1, in <module>
ips["srv-glpi"]
~~~^^^^^^^^^^^^
KeyError: 'srv-glpi'
>>> del ips["srv-nas"]
>>> list(ips.keys()), list(ips.values())
(['srv-ad', 'srv-ad2'], ['192.168.10.177', '192.168.10.181'])
- Lire :
d[cle]lève uneKeyErrorsi la clé n'existe pas ;d.get(cle)renvoieNone(la console n'affiche rien), etd.get(cle, defaut)renvoie la valeur par défaut. - Ajouter ou modifier :
d[cle] = valeur. Une clé est unique : affecter une clé existante remplace sa valeur. - Supprimer :
del d[cle], oud.pop(cle)qui renvoie la valeur retirée. - Tester :
cle in dcherche parmi les clés, pas parmi les valeurs.
Les clés doivent être non modifiables : chaînes, nombres, tuples. Une liste ne peut pas être une clé. Depuis Python 3.7, un dictionnaire garde l'ordre d'insertion de ses clés.
Parcourir un dictionnaire
# parcourir.py : parcourir un dictionnaire et le trier par valeur
occupation = {"srv-ad": 31.7, "srv-fichiers": 87.5, "srv-journaux": 93.0, "srv-gestion": 44.8}
for serveur, taux in occupation.items():
print(f"{serveur:<14}{taux:5.1f} %")
plus_plein = max(occupation, key=occupation.get)
print("Le plus plein :", plus_plein)
for serveur, taux in sorted(occupation.items(), key=lambda paire: paire[1], reverse=True):
print(serveur, taux)
srv-ad 31.7 %
srv-fichiers 87.5 %
srv-journaux 93.0 %
srv-gestion 44.8 %
Le plus plein : srv-journaux
srv-journaux 93.0
srv-fichiers 87.5
srv-gestion 44.8
srv-ad 31.7
items() donne les paires (clé, valeur), keys() les clés et values() les valeurs ; une boucle for cle in d parcourt les clés. max(occupation, key=occupation.get) cherche la clé dont la valeur est la plus grande. Pour trier un dictionnaire par valeur, on trie ses paires avec une clé de tri qui prend le deuxième élément.
Compter et regrouper
Compter des occurrences est l'usage le plus fréquent d'un dictionnaire : combien de postes par service, d'échecs de connexion par compte, de requêtes par adresse IP.
# compter.py : compter des occurrences, à la main puis avec Counter
from collections import Counter
services = ["Logistique", "Comptabilité", "Logistique", "Commercial",
"Logistique", "Comptabilité", "Direction"]
effectifs = {}
for service in services:
effectifs[service] = effectifs.get(service, 0) + 1
print(effectifs)
compteur = Counter(services)
print(compteur["Logistique"], compteur["Juridique"])
print(compteur.most_common(2))
{'Logistique': 3, 'Comptabilité': 2, 'Commercial': 1, 'Direction': 1}
3 0
[('Logistique', 3), ('Comptabilité', 2)]
La boucle à la main utilise get(service, 0) : 0 la première fois, puis la valeur déjà comptée. Counter, du module collections, fait la même chose en une ligne, renvoie 0 pour une clé absente au lieu d'une erreur, et most_common(n) donne les n plus fréquents.
Pour regrouper, il faut une liste par clé. defaultdict(list) crée automatiquement une liste vide à la première utilisation d'une clé :
# enregistrements.py : une liste de dictionnaires, puis regrouper avec defaultdict
from collections import defaultdict
machines = [
{"nom": "srv-ad", "ip": "192.168.10.177", "vlan": 20, "os": "Windows Server 2025"},
{"nom": "srv-journaux", "ip": "192.168.10.180", "vlan": 20, "os": "Debian 13"},
{"nom": "srv-web", "ip": "192.168.10.225", "vlan": 60, "os": "Debian 13"},
{"nom": "srv-recette", "ip": "192.168.21.186", "vlan": 21, "os": "Debian 13"},
{"nom": "srv-ad2", "ip": "192.168.10.181", "vlan": 20, "os": "Windows Server 2025"},
]
for m in machines:
if m["os"].startswith("Debian"):
print(m["nom"], m["ip"])
par_vlan = defaultdict(list)
for m in machines:
par_vlan[m["vlan"]].append(m["nom"])
for vlan in sorted(par_vlan):
print(f"VLAN {vlan} : {', '.join(par_vlan[vlan])}")
srv-journaux 192.168.10.180
srv-web 192.168.10.225
srv-recette 192.168.21.186
VLAN 20 : srv-ad, srv-journaux, srv-ad2
VLAN 21 : srv-recette
VLAN 60 : srv-web
Une liste de dictionnaires est la forme naturelle d'un tableau de données en Python : chaque dictionnaire est une ligne, chaque clé une colonne. C'est exactement ce que produit la lecture d'un fichier CSV avec csv.DictReader ou d'une réponse d'API en JSON (chapitre 7).
Les ensembles
Un ensemble ne contient chaque valeur qu'une fois, et il n'a pas d'ordre. Ses opérations sont celles des ensembles en mathématiques (union, intersection, complémentaire), ce qui en fait l'outil idéal pour comparer deux listes. Ici, les postes connus de l'annuaire Active Directory et ceux de l'inventaire :
>>> vlans = {20, 60, 21, 20, 10}
>>> vlans
{10, 20, 21, 60}
>>> annuaire = {"PC-0012", "PC-0015", "PC-0021", "PC-0042"}
>>> inventaire = {"PC-0012", "PC-0021", "PC-0042", "PC-0050"}
>>> sorted(annuaire & inventaire)
['PC-0012', 'PC-0021', 'PC-0042']
>>> sorted(annuaire | inventaire)
['PC-0012', 'PC-0015', 'PC-0021', 'PC-0042', 'PC-0050']
>>> annuaire - inventaire
{'PC-0015'}
>>> inventaire - annuaire
{'PC-0050'}
>>> sorted(annuaire ^ inventaire)
['PC-0015', 'PC-0050']
>>> "PC-0050" in inventaire
True
>>> len(set(["Logistique", "Direction", "Logistique"]))
2
| Opération | Écriture | Résultat ici |
|---|---|---|
| Intersection | a & b | Les postes connus des deux côtés |
| Union | a | b | Tous les postes, sans doublon |
| Différence | a - b | Dans l'annuaire mais pas dans l'inventaire : PC-0015 |
| Différence symétrique | a ^ b | D'un seul côté : les anomalies à traiter |
Les ensembles de chaînes s'affichent dans un ordre qui change d'une exécution à l'autre : on les passe à sorted() pour un affichage stable. Tester l'appartenance (x in ensemble) est quasi instantané, même avec un million d'éléments, alors que x in liste parcourt la liste. Attention, {} crée un dictionnaire vide ; l'ensemble vide s'écrit set().
Les compréhensions
Une compréhension construit une collection à partir d'une autre, en une ligne : « l'expression, pour chaque élément, si la condition est vraie ».
# comprehensions.py : construire une liste, un dictionnaire, un ensemble en une ligne
occupation = {"srv-ad": 31.7, "srv-fichiers": 87.5, "srv-journaux": 93.0, "srv-gestion": 44.8}
a_surveiller = [nom for nom, taux in occupation.items() if taux >= 80]
print(a_surveiller)
carres = [n ** 2 for n in range(1, 6)]
print(carres)
libre = {nom: round(100 - taux, 1) for nom, taux in occupation.items()}
print(libre)
saisis = [" SRV-AD ", "srv-ad2\n", "Srv-Fichiers", "srv-ad"]
uniques = {nom.strip().lower() for nom in saisis} # ensemble : sans doublon
print(sorted(uniques))
print(sum(1 for taux in occupation.values() if taux >= 80))
print(any(taux >= 90 for taux in occupation.values()), all(taux < 95 for taux in occupation.values()))
['srv-fichiers', 'srv-journaux']
[1, 4, 9, 16, 25]
{'srv-ad': 68.3, 'srv-fichiers': 12.5, 'srv-journaux': 7.0, 'srv-gestion': 55.2}
['srv-ad', 'srv-ad2', 'srv-fichiers']
2
True True
| Forme | Construit | Exemple |
|---|---|---|
[expr for x in seq if cond] | Une liste | Les serveurs à surveiller |
{cle: val for x in seq} | Un dictionnaire | L'espace libre par serveur |
{expr for x in seq} | Un ensemble | Les noms uniques, nettoyés |
(expr for x in seq) | Un générateur, consommé une fois | Dans sum(), any(), all(), sans parenthèses en plus |
any() répond « au moins un ? » et all() « tous ? ». Une compréhension remplace une boucle de trois lignes qui construit une collection, pas n'importe quelle boucle : si elle ne tient plus sur une ligne lisible, ou si elle sert à afficher, écris une boucle classique.
Choisir la bonne structure
| Ta question | La structure |
|---|---|
| Une suite d'éléments dans un ordre, qui change ? | Une liste |
| Quelques valeurs qui vont ensemble et ne bougent pas ? | Un tuple |
| Retrouver une valeur à partir d'un nom, d'un identifiant ? | Un dictionnaire |
| Compter, regrouper par catégorie ? | Counter, defaultdict(list) |
| Supprimer des doublons, comparer deux listes, tester vite l'appartenance ? | Un ensemble |
| Un tableau de données (lignes et colonnes) ? | Une liste de dictionnaires |
Exercices
Exercice 1 sur 3 Les comptes attaqués Réussi À revoir
Un extrait de journal donne les comptes visés par des échecs de connexion SSH :
echecs = ["admin", "clambert", "admin", "root", "admin", "mdiallo",
"root", "admin", "clambert", "admin", "test", "root"]
Affiche les trois comptes les plus visés avec leur nombre d'échecs, puis la liste triée des comptes qui ont au moins 3 échecs.
Voir la réponseMasquer la réponse de l'exercice 1
# ex_echecs.py : compter les échecs de connexion par compte
from collections import Counter
echecs = ["admin", "clambert", "admin", "root", "admin", "mdiallo",
"root", "admin", "clambert", "admin", "test", "root"]
compteur = Counter(echecs)
for compte, nombre in compteur.most_common(3):
print(f"{compte:<10}{nombre}")
suspects = sorted(compte for compte, nombre in compteur.items() if nombre >= 3)
print("Au moins 3 échecs :", suspects)
admin 5
root 3
clambert 2
Au moins 3 échecs : ['admin', 'root']
Les comptes génériques admin, root et test n'existent pas forcément sur le serveur : leur présence est la signature d'une attaque par force brute ou par dictionnaire. Le chapitre 11 extrait ces comptes et les adresses IP d'un vrai journal SSH, exporté avec journalctl.
Tu avais trouvé ?
Exercice 2 sur 3 Une adresse pour deux Réussi À revoir
Le fichier de suivi des adresses contient ce dictionnaire :
ips = {
"srv-ad": "192.168.10.177",
"srv-fichiers": "192.168.10.178",
"srv-test": "192.168.10.178",
"srv-nas": "192.168.10.183",
"srv-glpi": "192.168.10.184",
"vieux-nas": "192.168.10.183",
}
Écris un script qui affiche chaque adresse attribuée à plus d'une machine, avec les machines concernées.
Voir la réponseMasquer la réponse de l'exercice 2
On « retourne » le dictionnaire : pour chaque adresse, la liste des machines.
# ex_doublons.py : repérer les adresses IP attribuées à plusieurs machines
from collections import defaultdict
ips = {
"srv-ad": "192.168.10.177",
"srv-fichiers": "192.168.10.178",
"srv-test": "192.168.10.178",
"srv-nas": "192.168.10.183",
"srv-glpi": "192.168.10.184",
"vieux-nas": "192.168.10.183",
}
machines_par_ip = defaultdict(list)
for nom, ip in ips.items():
machines_par_ip[ip].append(nom)
conflits = {ip: noms for ip, noms in machines_par_ip.items() if len(noms) > 1}
for ip, noms in sorted(conflits.items()):
print(f"{ip} : {' et '.join(noms)}")
192.168.10.178 : srv-fichiers et srv-test
192.168.10.183 : srv-nas et vieux-nas
Un simple dictionnaire inversé {ip: nom} ne marcherait pas : la deuxième machine écraserait la première, et le conflit disparaîtrait. Deux machines sur la même adresse, c'est le conflit d'adresses qui coupe l'une ou l'autre du réseau de façon aléatoire.
Tu avais trouvé ?
Exercice 3 sur 3 Trois compréhensions Réussi À revoir
Avec cette liste de postes :
postes = [
{"nom": "PC-0012", "service": "Comptabilité", "fin_garantie": 2025},
{"nom": "PC-0015", "service": "Logistique", "fin_garantie": 2027},
{"nom": "PC-0021", "service": "Logistique", "fin_garantie": 2026},
{"nom": "PC-0042", "service": "Direction", "fin_garantie": 2024},
{"nom": "PC-0050", "service": "Logistique", "fin_garantie": 2028},
]
Écris en une compréhension chacune : (1) la liste des noms des postes dont la garantie a pris fin avant 2026 ; (2) le dictionnaire nom → année de fin de garantie ; (3) la liste triée des services, sans doublon.
Voir la réponseMasquer la réponse de l'exercice 3
# ex_postes.py : compréhensions sur une liste de postes
postes = [
{"nom": "PC-0012", "service": "Comptabilité", "fin_garantie": 2025},
{"nom": "PC-0015", "service": "Logistique", "fin_garantie": 2027},
{"nom": "PC-0021", "service": "Logistique", "fin_garantie": 2026},
{"nom": "PC-0042", "service": "Direction", "fin_garantie": 2024},
{"nom": "PC-0050", "service": "Logistique", "fin_garantie": 2028},
]
hors_garantie = [p["nom"] for p in postes if p["fin_garantie"] < 2026]
print(hors_garantie)
garantie_par_poste = {p["nom"]: p["fin_garantie"] for p in postes}
print(garantie_par_poste["PC-0021"])
services = sorted({p["service"] for p in postes})
print(services)
['PC-0012', 'PC-0042']
2026
['Comptabilité', 'Direction', 'Logistique']
Pour les services, une compréhension d'ensemble supprime les doublons, puis sorted() la transforme en liste triée.
Tu avais trouvé ?