Débutant

Fondamentaux Python

Syntaxe, variables, conditions, boucles, fonctions et structures de données — 4 modules pour poser des bases solides

[01]Module 1

Syntaxe, variables et types

Installer Python, écrire son premier script, nommer des variables et manipuler les types de base.

InstallationprintVariablesint / float / str / bool
1.1 Installer Python et écrire son premier script

Python est un langage interprété : le code source (.py) est exécuté ligne par ligne par l'interpréteur, sans étape de compilation. Téléchargez Python 3.12 depuis python.org en cochant « Add python.exe to PATH » sous Windows, puis vérifiez dans un terminal :

python --version
python -m pip --version
Pourquoi c'est important : En SOC comme en réseau, tu répètes les mêmes vérifications des dizaines de fois par jour. Savoir installer Python et lancer un script de façon fiable sur n'importe quelle machine, c'est la condition pour automatiser ces tâches pénibles au lieu de les subir.
L'analogie qui aide : Pense à une cuisine : avant de cuisiner, tu installes les plaques et tu testes qu'elles chauffent. Python, c'est la plaque ; print(), c'est faire bouillir de l'eau pour vérifier que tout fonctionne.

La fonction print() affiche du texte à l'écran. Les chaînes de caractères se délimitent par des guillemets simples ou doubles. Le symbole # démarre un commentaire ignoré par l'interpréteur :

print("Bonjour, automatisation !")
print('Adresse IP :', '192.168.1.1')
# Ceci est un commentaire : il documente le code
Démonstration pas à pas : Exécution de print("Bonjour, automatisation !") :
1. L'interpréteur lit la ligne. État : rien affiché.
2. Il évalue la chaîne "Bonjour, automatisation !". État : texte prêt.
3. Il appelle print(). État affiché : Bonjour, automatisation !
4. La ligne # Ceci est un commentaire est ignorée. État : aucun effet, le script continue.
Pièges classiques : Sous Windows, oublier de cocher « Add python.exe to PATH » rend la commande python introuvable. Confondre python et python3 selon la machine. Mélanger guillemets simples et doubles dans la même chaîne. Exécuter un autre fichier que celui qu'on vient de modifier.
À vous de jouer : Prédis la sortie de ce script, puis vérifie au terminal :
print('IP :', '10.0.0.1')
print("Scan", "terminé")
Voir la réponse

Sortie :
IP : 10.0.0.1
Scan terminé

Explication : chaque print() ajoute un retour à la ligne, et la virgule insère une espace entre les arguments.

Exercice 1.1

À faire : affichez votre prénom puis la phrase « Je débute en Python pour l'automatisation » sur deux lignes avec deux appels à print().
1.2 Variables et affectation

Une variable est un nom qui référence une valeur en mémoire. En Python, on ne déclare pas de type : l'affectation avec = crée la variable et l'interpréteur déduit le type. Les noms utilisent des minuscules et des underscores (snake_case) :

hostname = "SW1"
vlan_id = 10
actif = True
print(hostname, vlan_id, actif)
Pourquoi c'est important : Un script réseau manipule des hostnames, des VLAN et des compteurs d'alertes. Sans variables bien nommées, tu dois réécrire chaque valeur en dur et la moindre faute se propage partout.
L'analogie qui aide : Une variable, c'est une étiquette collée sur une boîte. hostname = "SW1" colle l'étiquette « hostname » sur la boîte qui contient « SW1 ». Si tu changes le contenu, l'étiquette suit.

On peut affecter plusieurs variables en une ligne, échanger deux valeurs sans variable temporaire, et utiliser les affectations augmentées (+=, -=, *=) très pratiques pour les compteurs :

ip, masque = "10.0.0.1", "255.255.255.0"
compteur = 0
compteur += 1      # compteur vaut 1
compteur *= 10     # compteur vaut 10
Démonstration pas à pas : Trace :
compteur = 0 → État : compteur vaut 0.
compteur += 1 → État : compteur vaut 1 (0 + 1).
compteur *= 10 → État : compteur vaut 10 (1 x 10).
ip, masque = "10.0.0.1", "255.255.255.0" → État : ip vaut 10.0.0.1, masque vaut 255.255.255.0.
Pièges classiques : Utiliser = (affectation) à la place de == (comparaison) dans un test. Mettre des tirets ou des espaces dans un nom (mon-routeur est invalide). Écraser une variable sans s'en rendre compte. Nommer en majuscules ce qui n'est pas une constante.
À vous de jouer : Prédis la sortie, puis vérifie :
a = 3
b = a
a = 5
print(a, b)
Voir la réponse

Sortie : 5 3.
Explication : b = a copie la valeur 3 au moment de l'affectation. Changer a ensuite ne change pas b.

Exercice 1.2

À faire : créez les variables routeur (« R1 »), interface (« Gi0/0 ») et up (True), puis affichez-les sur une seule ligne avec print().
1.3 Les types de base : int, float, str, bool

Les quatre types de base couvrent l'essentiel : int (entiers), float (décimaux), str (texte), bool (True/False). La fonction type() révèle le type d'une valeur et les fonctions int(), float(), str() convertissent d'un type à l'autre :

port = 22
bande_passante = 1.5
banniere = "Acces reserve"
print(type(port), type(bande_passante), type(banniere))
print(int("8080") + 1)      # 8081 : conversion puis calcul
print(str(404) + " introuvable")
Pourquoi c'est important : Tout ce qui sort d'un équipement ou d'un fichier arrive en texte. Si tu additionnes "8080" + 1 sans convertir, le script plante en pleine astreinte. Maîtriser les types, c'est éviter ces plantages.
L'analogie qui aide : Ce sont des ingrédients avec des unités : on ne mélange pas des grammes (int) et des litres (str) sans convertir. int() et str() sont la balance qui convertit.

Attention aux conversions impossibles : int("VLAN10") lève une erreur ValueError. En automatisation, les données lues sur un équipement arrivent toujours en texte : il faut les convertir avant de calculer (module 5 et chapitre des exceptions).

Démonstration pas à pas : Trace :
port = 22 → type int.
type(port)<class 'int'>.
int("8080") + 1 → conversion « 8080 » vers 8080, puis 8080 + 1 = 8081.
str(404) + " introuvable" → conversion 404 vers « 404 », puis concaténation : « 404 introuvable ».
Pièges classiques : int("VLAN10") lève ValueError car le texte n'est pas un nombre pur. "3" + 4 lève TypeError : on ne concatène pas du texte et un nombre sans conversion. Les float ont des approximations (0.1 + 0.2). bool("False") vaut True car toute chaîne non vide est vraie.
À vous de jouer : Prédis la sortie, puis vérifie :
print(int("21") * 2)
print(str(7) + str(7))
Voir la réponse

Sortie :
42
77

Explication : int("21") donne l'entier 21, 21 x 2 = 42. str(7) + str(7) concatène deux textes : « 7 » + « 7 » = « 77 », pas 14.

Exercice 1.3

À faire : stockez la chaîne "443" dans port_txt, convertissez-la en entier dans port, ajoutez 1 et affichez le résultat (attendu : 444).
1.4 Opérateurs arithmétiques et chaînes formatées

Python propose les opérateurs classiques + - * /, plus la division entière //, le modulo % (reste) et la puissance **. Le modulo sert par exemple à tester la parité ou à répartir des adresses :

print(7 / 2)    # 3.5 : division reelle
print(7 // 2)   # 3 : division entiere
print(7 % 2)    # 1 : reste
print(2 ** 8)   # 256 : 2 puissance 8 (un /24 = 256 adresses)
Pourquoi c'est important : Calculer le nombre d'hôtes d'un /26 ou générer 48 lignes de configuration à la main, c'est long et source d'erreurs. Les opérateurs et les f-strings font ces calculs et ces générations en une ligne fiable.
L'analogie qui aide : Les opérateurs sont la calculatrice, la f-string est le moule à lettre : tu écris le modèle une fois avec des trous { }, Python y verse les valeurs.

Les f-strings (préfixe f) injectent des variables dans un texte avec des accolades. C'est la façon moderne de générer des configurations et des rapports :

hote = "R1"
vlan = 20
print(f"Switch {hote} : port en VLAN {vlan}")
print(f"Masque /26 : {2 ** (32 - 26) - 2} hotes utilisables")
Démonstration pas à pas : Trace :
7 / 2 → 3.5 (division réelle).
7 // 2 → 3 (partie entière).
7 % 2 → 1 (reste).
2 ** 8 → 256.
hote = "R1" ; vlan = 20 → f-string : « Switch R1 : port en VLAN 20 ».
Pièges classiques : Confondre / et // fausse les calculs de préfixes. Oublier le préfixe f affiche les accolades au lieu des valeurs. Doubler les accolades pour afficher une accolade littérale. Ignorer les priorités : 2 ** (32 - 26) - 2 n'est pas 2 ** (32 - 26 - 2).
À vous de jouer : Prédis la sortie, puis vérifie :
print(10 % 3)
print(f"{2 ** 4} hotes")
Voir la réponse

Sortie :
1
16 hotes

Explication : 10 = 3 x 3 + 1, le reste vaut 1. 2 ** 4 vaut 16, injecté dans la f-string.

Exercice 1.4

À faire : avec prefix = 26, calculez et affichez via une f-string le nombre d'hôtes utilisables d'un /26 (attendu : 62).
[02]Module 2

Conditions et boucles

Prendre des décisions avec if/elif/else et répéter des actions avec for et while — le cœur de tout script.

if / elif / elseforwhilerange
2.1 Conditions if, elif, else

Une condition exécute un bloc seulement si un test est vrai. Les opérateurs de comparaison sont == != < > <= >= ; on les combine avec and, or et not. Le bloc est délimité par l'indentation (4 espaces) après les deux-points :

tentatives = 15
if tentatives > 10:
    print("ALERTE : inviter a verifier cette IP")
elif tentatives > 5:
    print("Surveillance renforcee")
else:
    print("Trafic normal")
Pourquoi c'est important : Un script SOC décide seul : plus de 10 tentatives = alerte, sinon trafic normal. Sans conditions fiables, tu dois relire chaque log à la main, et tu rates l'attaque à 3 heures du matin.
L'analogie qui aide : La condition, c'est le vigile à l'entrée : il regarde le badge (le test) et ouvre la porte du bloc concerné, ou la porte « sinon ».

En Python, les valeurs « fausses » sont : False, None, 0, "" (chaîne vide), [], {}. On peut donc tester directement if reponse: pour vérifier qu'une chaîne n'est pas vide.

Démonstration pas à pas : Trace avec tentatives = 15 :
1. Test 15 > 10 → True, on entre dans le premier bloc.
2. Affiche « ALERTE ». État : les blocs elif et else sont sautés.
Avec tentatives = 7 : premier test False, second 7 > 5 True → « Surveillance renforcee ». Avec 2 : les deux False → « Trafic normal ».
Pièges classiques : Écrire = au lieu de == dans le test. Oublier les deux-points en fin de ligne if. Indenter avec un mélange de tabulations et d'espaces. Tester if reponse == True au lieu de if reponse, ou mal chaîner and / or.
À vous de jouer : Prédis la sortie, puis vérifie :
x = 7
if x > 10:
print("haut")
elif x > 5:
print("moyen")
else:
print("bas")
Voir la réponse

Sortie : moyen.
Explication : 7 n'est pas > 10 (False) mais est > 5 (True), donc c'est le bloc elif qui s'exécute.

Exercice 2.1

À faire : si port = 22 affichez « SSH », si port = 80 affichez « HTTP », sinon affichez « Port inconnu ». Testez avec les trois valeurs.
2.2 Boucle for et fonction range()

La boucle for parcourt chaque élément d'une séquence. La fonction range(n) génère les entiers de 0 à n-1, range(debut, fin) un intervalle, range(debut, fin, pas) avec un pas personnalisé :

for i in range(5):
    print(f"Tentative {i + 1}/5")

for octet in range(1, 6):
    print(f"Scan de 192.168.1.{octet}")
Pourquoi c'est important : Scanner 254 adresses ou pousser une commande sur 40 équipements : à la main, tu y passes la journée. Une boucle for fait le même travail en quelques secondes, sans faute de frappe.
L'analogie qui aide : C'est la tournée du facteur : pour chaque boîte (chaque élément), il dépose le même courrier (le bloc d'instructions), l'une après l'autre.

On peut aussi parcourir directement une liste de chaînes — idéal pour traiter une liste d'équipements ou d'adresses :

equipements = ["R1", "SW1", "SW2"]
for hote in equipements:
    print(f"Connexion a {hote}...")
Démonstration pas à pas : Trace de for i in range(5) :
i vaut 0 → affiche « Tentative 1/5 » (i + 1).
i vaut 1 → « Tentative 2/5 ».
... jusqu'à i = 4 → « Tentative 5/5 ». État final : la boucle s'arrête, range(5) n'a produit que 0 à 4.
range(1, 6) produit 1, 2, 3, 4, 5 : la borne de fin est exclue.
Pièges classiques : range(5) commence à 0, pas à 1. La borne de fin est exclue : range(1, 10) s'arrête à 9. Une indentation fausse sort une ligne de la boucle. Modifier une liste pendant qu'on la parcourt saute des éléments.
À vous de jouer : Prédis la sortie, puis vérifie :
for i in range(3):
print(i)
Voir la réponse

Sortie :
0
1
2

Explication : range(3) produit 0, 1, 2. Il n'y a jamais de 3.

Exercice 2.2

À faire : affichez les adresses 10.0.0.1 à 10.0.0.10 avec une boucle for et range().
2.3 Boucle while et compteurs

La boucle while répète un bloc tant qu'une condition est vraie. Elle sert quand on ne connaît pas à l'avance le nombre d'itérations (attente d'un état, nouvelles tentatives). Il faut toujours faire évoluer la condition, sinon la boucle est infinie :

essais = 0
while essais < 3:
    print(f"Essai {essais + 1}")
    essais += 1
print("Fin des essais")
Pourquoi c'est important : En production, tu ne sais pas à l'avance combien d'essais il faudra pour joindre un équipement. while réessaie jusqu'au succès, break stoppe dès qu'un port critique répond.
L'analogie qui aide : C'est frapper à une porte jusqu'à ce qu'on ouvre (while), avec le droit de partir tout de suite si on entend l'alarme (break) ou de sauter une porte condamnée (continue).

Les instructions break (sortir immédiatement) et continue (sauter à l'itération suivante) contrôlent finement les deux types de boucles :

for code in [200, 301, 404, 500, 200]:
    if code == 500:
        print("Erreur serveur : arret")
        break
    if code == 301:
        continue  # on ignore les redirections
    print(f"Code traite : {code}")
Démonstration pas à pas : Trace : essais = 0 → test 0 < 3 True → affiche « Essai 1 », essais passe à 1 → test True → « Essai 2 », essais = 2 → test True → « Essai 3 », essais = 3 → test 3 < 3 False → sortie, affiche « Fin des essais ».
Avec break : codes 200 traité, 301 sauté par continue, 404 traité, 500 → message puis sortie immédiate, le dernier 200 n'est jamais traité.
Pièges classiques : Oublier essais += 1 crée une boucle infinie. Inverser break et continue. Mettre un while True sans aucune porte de sortie. Comparer avec = dans la condition du while.
À vous de jouer : Prédis la sortie, puis vérifie :
n = 3
while n > 0:
print(n)
n -= 1
Voir la réponse

Sortie :
3
2
1

Explication : tant que n > 0, on affiche puis on décrémente. Quand n vaut 0, le test devient False et la boucle s'arrête.

Exercice 2.3

À faire : avec while, affichez un compte à rebours de 5 à 1 puis « Décollage du scan ! ». Utilisez break pour stopper une boucle for sur les ports [22, 80, 443, 3389] dès que le port 443 est atteint.
[03]Module 3

Fonctions

Découper son code en fonctions réutilisables : paramètres, valeurs de retour, portée des variables.

defreturnParamètresPortée
3.1 Définir et appeler une fonction

Une fonction est un bloc nommé qui effectue une tâche. On la définit avec def, on l'appelle par son nom suivi de parenthèses. Les paramètres sont les valeurs reçues entre parenthèses :

def saluer(nom):
    print(f"Bonjour {nom} !")

saluer("Alice")
saluer("Bob")
Pourquoi c'est important : Générer une ligne de configuration pour un port, c'est utile ; le faire pour 200 ports sans fonction, c'est 200 copier-coller à maintenir. Une fonction écrit la logique une fois et l'applique partout.
L'analogie qui aide : C'est une machine à café : tu appuies sur le bouton avec ta dosette (l'argument nom), elle sort toujours le même café formaté, sans refaire la recette.

Une fonction peut avoir des paramètres avec valeurs par défaut : l'appelant peut les omettre. La docstring (chaîne sous le def) documente le rôle de la fonction :

def generer_interface(nom, vlan=10):
    """Genere une ligne de configuration de port."""
    print(f"interface {nom} : access VLAN {vlan}")

generer_interface("Fa0/1")
generer_interface("Fa0/2", 20)
Démonstration pas à pas : Trace :
def saluer(nom) → définit la machine, rien ne s'affiche.
saluer("Alice") → paramètre nom = « Alice » → affiche « Bonjour Alice ! ».
saluer("Bob") → nom = « Bob » → « Bonjour Bob ! ».
generer_interface("Fa0/1") → vlan garde son défaut 10 → « interface Fa0/1 : access VLAN 10 ».
Pièges classiques : Appeler sans parenthèses (saluer au lieu de saluer("Alice")) ne fait rien. Oublier les deux-points après def. Inverser l'ordre des paramètres. Croire que la fonction s'exécute à sa définition : elle ne s'exécute qu'à l'appel.
À vous de jouer : Prédis la sortie, puis vérifie :
def banniere(hote):
print(f"=== {hote} ===")
banniere("R1")
banniere("SW1")
Voir la réponse

Sortie :
=== R1 ===
=== SW1 ===

Explication : chaque appel rejoue le même bloc avec une valeur différente de hote.

Exercice 3.1

À faire : écrivez la fonction banniere(hote) qui affiche « === Configuration de {hote} === » et appelez-la pour « R1 » et « SW1 ».
3.2 Valeurs de retour avec return

L'instruction return renvoie un résultat à l'appelant et termine la fonction. Sans return, une fonction renvoie None. Séparer le calcul (fonction qui retourne) de l'affichage (print) rend le code testable et réutilisable :

def hotes_utilisables(prefix):
    return 2 ** (32 - prefix) - 2

print(hotes_utilisables(24))  # 254
print(hotes_utilisables(26))  # 62
Pourquoi c'est important : Afficher un résultat, c'est bien ; le réutiliser dans un test, un rapport ou un autre calcul, c'est mieux. return rend la fonction réutilisable et testable au lieu de l'enfermer dans un print.
L'analogie qui aide : Le print, c'est crier le résultat par la fenêtre. Le return, c'est rendre la monnaie au client : il peut s'en servir pour la suite.

Une fonction peut retourner plusieurs valeurs sous forme de tuple, et retourner tôt pour les cas particuliers (garde) :

def analyser_code(code):
    if code == 200:
        return "OK", True
    if code == 404:
        return "Introuvable", False
    return "Autre", False

message, succes = analyser_code(404)
print(message, succes)
Démonstration pas à pas : Trace : hotes_utilisables(24) → prefix = 24 → 2 ** (32 - 24) - 2 = 256 - 2 = 254 → la fonction renvoie 254, et print() l'affiche.
analyser_code(404) → premier test False, second True → renvoie le tuple ("Introuvable", False) → déballé en message = « Introuvable », succes = False.
Pièges classiques : Oublier return : la fonction renvoie None. Mettre return trop tôt dans une boucle : seul le premier élément est traité. Confondre afficher et renvoyer. Déballer le mauvais nombre de valeurs d'un tuple.
À vous de jouer : Prédis la sortie, puis vérifie :
def f(a):
return a + 1
print(f(41))
Voir la réponse

Sortie : 42.
Explication : f(41) renvoie 42 à l'appelant, et print() affiche cette valeur retournée.

Exercice 3.2

À faire : écrivez est_prive(ip) qui retourne True si l'adresse commence par « 192.168. » ou « 10. », False sinon. Testez avec « 192.168.1.5 » et « 8.8.8.8 ».
3.3 Portée des variables et bonnes pratiques

Une variable créée dans une fonction est locale : elle n'existe pas à l'extérieur. Une variable créée au niveau du script est globale et lisible partout. On évite de modifier des globales depuis une fonction : on préfère passer des paramètres et retourner des résultats :

seuil = 10  # globale : lisible dans les fonctions

def alerter(ip, tentatives):
    message = f"{ip} suspect"  # locale : invisible dehors
    return tentatives > seuil and message or "OK"
Pourquoi c'est important : En production, une variable locale qui écrase un seuil global par accident transforme un script de détection en passoire. Comprendre la portée, c'est éviter ces corruptions silencieuses.
L'analogie qui aide : La variable globale, c'est l'affiche au mur de la salle (visible par tous). La variable locale, c'est la note dans ton carnet personnel : personne d'autre ne la voit une fois la fonction terminée.
  • Un nom de fonction = un verbe d'action (extraire_ip, generer_rapport).
  • Une fonction = une seule tâche, courte (moins de 20 lignes si possible).
  • Nommez les paramètres explicitement à l'appel pour les fonctions à plusieurs arguments.
Démonstration pas à pas : Trace : seuil = 10 (globale). Appel alerter("5.6.7.8", 42) → paramètres ip et tentatives créés en local, message créé en local, lecture de seuil autorisée → test 42 > 10 True → renvoie « 5.6.7.8 suspect ». Après l'appel, message n'existe plus dehors.
Pièges classiques : Affecter une globale dans une fonction sans global crée en fait une locale et peut lever UnboundLocalError. Abuser de global rend le code fragile. Donner à une locale le même nom qu'une globale masque la globale. Lire une locale hors de sa fonction lève NameError.
À vous de jouer : Prédis la sortie, puis vérifie :
x = 10
def f():
x = 5
return x
print(x)
print(f())
print(x)
Voir la réponse

Sortie :
10
5
10

Explication : le x dans la fonction est local et ne touche pas au x global, qui reste à 10 avant comme après l'appel.

Exercice 3.3

À faire : écrivez classer_tentatives(n) qui retourne « critique » si n > 50, « suspect » si n > 10, « normal » sinon. Testez avec 3, 25 et 100.
[04]Module 4

Structures de données et compréhensions

Listes, tuples, sets, dictionnaires et compréhensions : stocker et transformer des collections efficacement.

listdictset / tupleCompréhensions
4.1 Listes : la collection ordonnée et modifiable

Une liste stocke plusieurs valeurs entre crochets. Les éléments sont indexés à partir de 0, les index négatifs partent de la fin. Les méthodes append(), remove(), sort() et la fonction len() sont incontournables :

ports = [22, 80, 443]
ports.append(8080)
print(ports[0], ports[-1], len(ports))
for p in ports:
    print(f"Scan du port {p}")
Pourquoi c'est important : Un scan porte sur une liste de ports, une ronde sur une liste d'IP. Sans listes, tu déclares 50 variables et tu ne peux plus boucler : le script devient impossible à maintenir.
L'analogie qui aide : C'est un tiroir numéroté : chaque case a un numéro (l'index dès 0), tu ranges, tu sors la case 0 ou la dernière avec -1, et len() te dit combien de cases sont remplies.

Le slicing liste[debut:fin] extrait une sous-liste, et l'opérateur in teste la présence d'un élément :

logs = ["OK", "OK", "FAIL", "OK"]
print(logs[1:3])        # ['OK', 'FAIL']
print("FAIL" in logs)   # True
Démonstration pas à pas : Trace : ports = [22, 80, 443] → 3 éléments.
ports.append(8080) → [22, 80, 443, 8080].
ports[0] → 22, ports[-1] → 8080, len(ports) → 4.
logs[1:3] sur [« OK », « OK », « FAIL », « OK »] → [« OK », « FAIL »] (index 1 inclus, 3 exclu).
Pièges classiques : Les index commencent à 0 : ports[4] sur 4 éléments lève IndexError. remove(x) sur une valeur absente lève ValueError. b = a ne copie pas la liste, les deux noms partagent la même (utilise a.copy()). Le slicing ne modifie pas la liste d'origine.
À vous de jouer : Prédis la sortie, puis vérifie :
a = [1, 2, 3]
print(a[1:])
print(len(a))
Voir la réponse

Sortie :
[2, 3]
3

Explication : a[1:] part de l'index 1 jusqu'à la fin, et la longueur reste 3.

Exercice 4.1

À faire : créez la liste ips = ["10.0.0.1", "10.0.0.2"], ajoutez « 10.0.0.3 », affichez sa longueur puis chaque adresse avec une boucle.
4.2 Tuples et sets : immuable et unicité

Un tuple (parenthèses) est une liste immuable : parfait pour des valeurs fixes comme une adresse et son masque. Un set (accolades) ne contient que des valeurs uniques et supporte les opérations d'ensembles :

reseau = ("192.168.1.0", 24)   # tuple : on ne le modifiera pas
print(f"{reseau[0]}/{reseau[1]}")

vues_jour1 = {"10.0.0.5", "10.0.0.9", "10.0.0.5"}
print(vues_jour1)  # {'10.0.0.5', '10.0.0.9'} : doublon elimine
Pourquoi c'est important : Un couple adresse/masque ne doit jamais changer par accident en cours de script, et une liste de 10 000 IP observées contient des doublons qui faussent les comptages. Tuple et set règlent ces deux problèmes.
L'analogie qui aide : Le tuple, c'est une plaque gravée : on la lit mais on ne la modifie pas. Le set, c'est un sac qui refuse les doublons : tu peux y verser dix fois la même bille, il n'en garde qu'une.

Les sets excellent pour comparer des collections : intersection (&), différence (-), union (|). En SOC, on les utilise pour croiser des IP observées avec une liste d'IOC :

observees = {"1.2.3.4", "5.6.7.8", "9.9.9.9"}
ioc = {"5.6.7.8", "9.9.9.9"}
print(observees & ioc)  # IP malveillantes detectees
Démonstration pas à pas : Trace : reseau = ("192.168.1.0", 24)reseau[0] = 192.168.1.0, reseau[1] = 24.
{"10.0.0.5", "10.0.0.9", "10.0.0.5"} → le doublon est éliminé → {« 10.0.0.5 », « 10.0.0.9 »}.
observees & ioc → garde uniquement les IP présentes des deux côtés : {« 5.6.7.8 », « 9.9.9.9 »}.
Pièges classiques : Un tuple à un élément s'écrit (x,) avec la virgule, sinon ce sont juste des parenthèses. Un set n'a pas d'ordre : ne compte pas sur l'ordre d'affichage. On ne peut pas mettre une liste (modifiable) dans un set. Modifier un tuple lève TypeError.
À vous de jouer : Prédis la sortie, puis vérifie :
s = {1, 2, 2, 3}
print(len(s))
print(sorted(s))
Voir la réponse

Sortie :
3
[1, 2, 3]

Explication : le doublon 2 est éliminé, il reste 3 valeurs uniques, triées par sorted().

Exercice 4.2

À faire : à partir de tentatives = ["1.1.1.1", "2.2.2.2", "1.1.1.1"], construisez un set et affichez le nombre d'adresses uniques (attendu : 2).
4.3 Dictionnaires : des paires clé-valeur

Un dictionnaire associe des clés à des valeurs entre accolades. C'est LA structure de l'automatisation : configs d'équipements, réponses d'API JSON, compteurs. On accède avec d[cle] (ou d.get(cle, defaut) sans erreur si absent) :

equipement = {"hostname": "R1", "ip": "10.0.0.1", "os": "IOS"}
print(equipement["hostname"])
equipement["vlan"] = 10
for cle, valeur in equipement.items():
    print(f"{cle} : {valeur}")
Pourquoi c'est important : Fiche équipement, ligne d'inventaire, réponse d'API : en automatisation, presque tout est un dictionnaire. Si tu ne maîtrises pas les clés et les accès sûrs, ton script plante dès qu'un champ manque.
L'analogie qui aide : C'est un meuble à casiers étiquetés : tu n'ouvres pas le casier numéro 3, tu ouvres le casier « hostname ». Chaque étiquette (clé) mène direct à son contenu (valeur).

Les méthodes utiles : .keys(), .values(), .items() pour parcourir, .get() pour un accès sûr, in pour tester une clé. On peut imbriquer listes et dictionnaires pour modéliser un parc entier :

parc = [
    {"hostname": "R1", "ip": "10.0.0.1"},
    {"hostname": "SW1", "ip": "10.0.0.2"},
]
for eq in parc:
    print(f"{eq['hostname']} -> {eq['ip']}")
Démonstration pas à pas : Trace : equipement = {"hostname": "R1", "ip": "10.0.0.1", "os": "IOS"}equipement["hostname"] = « R1 ».
equipement["vlan"] = 10 → ajoute un casier. État : 4 paires.
Boucle for cle, valeur in equipement.items() → affiche chaque étiquette et son contenu, une ligne par paire.
Pièges classiques : Accéder à une clé absente avec d[cle] lève KeyError : préfère d.get(cle, defaut). Les clés doivent être uniques : un doublon écrase le précédent. Oublier les guillemets autour d'une clé chaîne. Confondre eq['ip'] et eq["ip"] dans une f-string mal quotée.
À vous de jouer : Prédis la sortie, puis vérifie :
d = {"a": 1, "b": 2}
print(d.get("c", 0))
print(sorted(d.keys()))
Voir la réponse

Sortie :
0
['a', 'b']

Explication : la clé « c » est absente donc get() renvoie le défaut 0 sans erreur. Les clés existantes sont « a » et « b ».

Exercice 4.3

À faire : créez le dictionnaire alerte = {"ip": "5.6.7.8", "tentatives": 42}, ajoutez la clé « niveau » valant « critique », puis affichez chaque paire clé-valeur.
4.4 Compréhensions de listes et de dictionnaires

Les compréhensions construisent une collection en une ligne : [expression for x in iterable if condition]. C'est l'idiome Python pour filtrer et transformer des données :

ports = [22, 80, 443, 8080, 3389]
ouverts = [p for p in ports if p < 1024]
print(ouverts)  # [22, 80, 443]

lignes = ["OK", "FAIL", "OK", "TIMEOUT"]
erreurs = [l for l in lignes if l != "OK"]
print(erreurs)
Pourquoi c'est important : Filtrer 5 000 lignes de logs pour ne garder que les erreurs prend 10 lignes avec une boucle classique. Une compréhension fait la même chose en une ligne lisible, et c'est l'idiome que tu liras dans tout code Python professionnel.
L'analogie qui aide : C'est un tamis : tu verses le mélange (l'itérable), seules les pépites qui passent le filtre (if) tombent transformées (l'expression) dans le nouveau récipient.

Même principe pour les dictionnaires et les sets. On combine souvent compréhension + split() pour parser du texte :

texte = "R1,SW1,SW2"
hotetes = [h.strip() for h in texte.split(",")]
compteurs = {h: 0 for h in hotetes}
print(compteurs)  # {'R1': 0, 'SW1': 0, 'SW2': 0}
Démonstration pas à pas : Trace avec ports = [22, 80, 443, 8080, 3389] :
22 < 1024 → gardé. 80 → gardé. 443 → gardé. 8080 → rejeté. 3389 → rejeté. Résultat : [22, 80, 443].
"R1,SW1,SW2".split(",") → [« R1 », « SW1 », « SW2 »] → {h: 0 for h in ...} → {« R1 »: 0, « SW1 »: 0, « SW2 »: 0}.
Pièges classiques : Inverser l'ordre (if avant for) est une erreur de syntaxe. Empiler trois conditions dans une compréhension la rend illisible : repasse à une boucle. En HTML, < dans le code doit rester échappé, sinon le navigateur le mange. Oublier .strip() après split() laisse des espaces parasites.
À vous de jouer : Prédis la sortie, puis vérifie :
print([x * 2 for x in [1, 2, 3] if x > 1])
Voir la réponse

Sortie : [4, 6].
Explication : 1 est rejeté par le filtre, 2 et 3 sont gardés puis doublés : 2 x 2 = 4, 3 x 2 = 6.

Exercice 4.4

À faire : à partir de codes = [200, 404, 200, 500, 301, 500], construisez par compréhension la liste des codes supérieurs ou égaux à 400 (attendu : [404, 500, 500]).