Aller au contenu
Réussir mon BTS SIO

Tape une notion, une commande ou le nom d'une épreuve.

Par exemple

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 :

python
>>> 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 une KeyError si la clé n'existe pas ; d.get(cle) renvoie None (la console n'affiche rien), et d.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], ou d.pop(cle) qui renvoie la valeur retirée.
  • Tester : cle in d cherche 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

python
# 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)
text
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.

python
# 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))
text
{'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é :

python
# 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])}")
text
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 :

python
>>> 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ÉcritureRésultat ici
Intersectiona & bLes postes connus des deux côtés
Uniona | bTous les postes, sans doublon
Différencea - bDans l'annuaire mais pas dans l'inventaire : PC-0015
Différence symétriquea ^ bD'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 ».

python
# 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()))
text
['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
FormeConstruitExemple
[expr for x in seq if cond]Une listeLes serveurs à surveiller
{cle: val for x in seq}Un dictionnaireL'espace libre par serveur
{expr for x in seq}Un ensembleLes noms uniques, nettoyés
(expr for x in seq)Un générateur, consommé une foisDans 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

Liste[1, 2, 3]ordonnéemodifiabledoublons permisl[0]les noms desserveursTuple(1, 2, 3)ordonnénon modifiabledoublons permist[0]un poste : nom,adresse, serviceDictionnaire{"a": 1}ordre d'insertionmodifiableclés uniquesd["a"]l'adresse IP dechaque serveurEnsemble{1, 2, 3}sans ordremodifiablesans doublonx in eles postes connusde l'annuaire
Liste, tuple, dictionnaire, ensemble : quatre structures pour quatre besoins
Ta questionLa 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 :

python
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
python
# 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)
text
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 :

python
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.

python
# 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)}")
text
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 :

python
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
python
# 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)
text
['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é ?

Chapitre terminé !

Fais les 3 exercices avant de passer à la suite : c'est en t'entraînant que tout se fixe.

chapitres
–
de série
–
du cours
–
Chapitre suivant : Lire et écrire des fichiers : pathlib, CSV et JSONRevoir le sommaire du cours