Programmation Python

Progression
NB3BN Info - Centre de Formation Informatique au Maroc
Programmation Python

Manuel de formation complet - 12 sections - Formation théorique et pratique

12
Sections
3
Niveau
50+
Pages
2024
Édition
Commencer la Formation
Section 1

Introduction à Python — Installation et Premiers Pas

Pourquoi Apprendre Python ?

Python est le langage de programmation le plus populaire au monde en 2024. Sa syntaxe claire, semblable à l'anglais naturel, en fait le meilleur choix pour les débutants. Les professionnels l'utilisent pour des applications critiques dans de nombreux domaines.

Python est utilisé pour :
🌐 Développement Web — Django, Flask, FastAPI
📊 Data Science — Analyse de données avec Pandas, NumPy
🤖 Intelligence Artificielle — TensorFlow, PyTorch, scikit-learn
⚙️ Automatisation — Scripts, bots, tâches répétitives
🔬 Sciences — Calcul scientifique, recherche académique

Installation de Python

1
Télécharger : Aller sur python.org/downloads → Télécharger Python 3.12+
2
Installer : Cocher "Add Python to PATH" lors de l'installation sur Windows
3
Vérifier : Ouvrir le terminal et taper python --version
4
IDE : Installer VS Code + extension "Python" de Microsoft

Premier Programme Python

# ── Affichage ──
print("Bonjour NB3BN Info !")
print("Python", 3.12, "est formidable !")
print("Résultat :", 5 + 3 * 2)  # 11

# ── Saisie utilisateur ──
nom = input("Entrez votre nom : ")
age_str = input("Entrez votre âge : ")
age = int(age_str)  # Convertir string → int

# ── f-strings (Python 3.6+) ──
print(f"Bonjour {nom}, vous avez {age} ans.")
print(f"Dans 10 ans, vous aurez {age + 10} ans.")
print(f"Votre nom en majuscules : {nom.upper()}")

# ── Commentaires ──
# Ceci est un commentaire sur une ligne
"""
Ceci est une docstring ou
un commentaire multi-lignes
"""

L'Interpréteur Python Interactif

# Dans le terminal, taper "python" pour ouvrir le REPL :
>>> 2 + 2
4
>>> "Python" * 3
'PythonPythonPython'
>>> help(print)   # Documentation intégrée
>>> exit()        # Quitter le REPL

pip — Gestionnaire de Paquets

# pip = Python Package Installer
pip install requests          # Installer un paquet
pip install requests==2.31.0  # Version spécifique
pip uninstall requests         # Désinstaller
pip list                       # Lister les paquets installés
pip freeze > requirements.txt  # Exporter les dépendances
pip install -r requirements.txt # Installer depuis un fichier

Environnements Virtuels — Bonne Pratique

# Créer un environnement virtuel isolé pour chaque projet
python -m venv env           # Créer l'environnement "env"

# Activer (Windows)
env\Scripts\activate

# Activer (macOS / Linux)
source env/bin/activate

# Désactiver
deactivate

# L'environnement actif est indiqué dans le terminal : (env) $
Bonne pratique : Toujours créer un environnement virtuel par projet pour éviter les conflits de dépendances entre projets.

Question : Quelle commande installe le paquet "requests" avec pip ?

python install requests
pip install requests ✓
npm install requests
apt install requests
Section 2

Variables, Types de Données et Opérateurs

Variables et Affectation

En Python, les variables sont créées automatiquement lors de l'affectation. Il n'est pas nécessaire de déclarer le type — Python est un langage à typage dynamique.

# ── Affectation simple ──
nom      = "NB3BN Info"     # str
age      = 5                 # int
prix     = 1500.50           # float
actif    = True              # bool
rien     = None              # NoneType

# ── Affectation multiple ──
x, y, z = 1, 2, 3
a = b = c = 0      # Toutes à 0
premier, *reste = [1, 2, 3, 4, 5]  # premier=1, reste=[2,3,4,5]

# ── Conventions de nommage ──
nom_complet = "Ali"     # snake_case — style Python recommandé
NOM_SITE    = "NB3BN"  # MAJUSCULES — constantes
MaClasse    = None      # PascalCase — classes

# ── Vérifier le type ──
print(type(nom))    # <class 'str'>
print(type(age))    # <class 'int'>
isinstance(nom, str)  # True

Types Numériques

# ── Entiers (int) — taille illimitée ──
petit   = 42
grand   = 9_999_999_999  # _ pour la lisibilité
binaire = 0b1010         # 10 en base 2
octal   = 0o17           # 15 en base 8
hexa    = 0xFF           # 255 en hexadécimal

# ── Flottants (float) ──
pi      = 3.14159265
sci     = 1.5e10     # 15000000000.0 (notation scientifique)
precise = round(1/3, 4)  # 0.3333

# ── Complexes ──
z = 3 + 4j  # Partie réelle + partie imaginaire

# ── Opérateurs arithmétiques ──
print(10 + 3)   # 13  Addition
print(10 - 3)   # 7   Soustraction
print(10 * 3)   # 30  Multiplication
print(10 / 3)   # 3.333... Division réelle
print(10 // 3)  # 3   Division entière (floor)
print(10 % 3)   # 1   Modulo (reste)
print(10 ** 3)  # 1000 Puissance

# ── Bibliothèque math ──
import math
print(math.sqrt(144))   # 12.0
print(math.ceil(4.2))   # 5
print(math.floor(4.9))  # 4
print(math.pi)          # 3.141592...
print(math.factorial(5)) # 120

Chaînes de Caractères (str)

# ── Création ──
simple    = 'Bonjour'
double    = "Python est génial"
multiligne = """Première ligne
Deuxième ligne
Troisième ligne"""

# ── Méthodes de chaînes ──
texte = "  Bonjour NB3BN Info  "
print(texte.strip())         # "Bonjour NB3BN Info"
print(texte.upper())         # "  BONJOUR NB3BN INFO  "
print(texte.lower())         # "  bonjour nb3bn info  "
print(texte.replace("Bonjour", "Salut"))
print(texte.split())         # ['Bonjour', 'NB3BN', 'Info']
print(texte.startswith("  Bonjour"))  # True
print(len(texte))            # 22

# ── Indexation et slicing ──
s = "Python"
print(s[0])     # 'P' (premier)
print(s[-1])    # 'n' (dernier)
print(s[1:4])   # 'yth' (de 1 à 3 inclus)
print(s[:3])    # 'Pyt'
print(s[3:])    # 'hon'
print(s[::-1])  # 'nohtyP' (inverser)

# ── Formatage ──
nom, note = "Sara", 17.5
f"Étudiante : {nom}, Note : {note:.2f}/20"  # f-string (recommandé)
"{} a eu {}".format(nom, note)              # .format()
"%-10s: %.1f" % (nom, note)                 # % (ancien style)

Booléens et Opérateurs Logiques

# ── Valeurs booléennes ──
vrai = True
faux = False

# Falsy values : 0, 0.0, "", [], {}, (), None, False
# Truthy values : tout le reste

# ── Opérateurs de comparaison ──
5 > 3     # True
5 == 5    # True  (égalité)
5 != 3    # True  (différent)
5 >= 5    # True
"a" < "b" # True  (ordre alphabétique)

# ── Opérateurs logiques ──
True and False  # False
True or False   # True
not True        # False

# ── Opérateurs d'appartenance et d'identité ──
"Py" in "Python"      # True
42 not in [1, 2, 3]   # True
x is None             # True si x est None (identity)
x is not None         # True si x n'est pas None

Conversion de Types

# ── Conversions explicites ──
int("42")        # 42
float("3.14")    # 3.14
str(100)         # "100"
bool(0)          # False
bool("texte")    # True
list("abc")      # ['a', 'b', 'c']
tuple([1,2,3])   # (1, 2, 3)
set([1,1,2,3])   # {1, 2, 3}

# ── Gestion des erreurs de conversion ──
try:
    nombre = int(input("Entrez un nombre : "))
except ValueError:
    print("Ce n'est pas un nombre valide !")

Question : Quel est le résultat de 17 // 5 en Python ?

3.4
3 ✓
2
17.5
Section 3

Structures de Contrôle — Conditions et Boucles

Instructions Conditionnelles

Les structures de contrôle permettent d'exécuter du code différemment selon des conditions.

# ── if / elif / else ──
note = 16

if note >= 18:
    mention = "Très Bien"
    print("Félicitations !")
elif note >= 16:
    mention = "Bien"
elif note >= 14:
    mention = "Assez Bien"
elif note >= 10:
    mention = "Passable"
else:
    mention = "Insuffisant"
    print("Vous devez rattraper l'examen.")

print(f"Note : {note}/20 — Mention : {mention}")

# ── Opérateur ternaire (expression conditionnelle) ──
statut = "Admis" if note >= 10 else "Refusé"
categorie = "Adulte" if age >= 18 else ("Adolescent" if age >= 12 else "Enfant")

# ── Match/Case (Python 3.10+, équivalent switch) ──
commande = "start"
match commande:
    case "start":  print("Démarrage...")
    case "stop":   print("Arrêt...")
    case "status": print("En cours...")
    case _:        print("Commande inconnue")

Boucles for

# ── Itérer sur une séquence ──
fruits = ["pomme", "banane", "orange", "mangue"]
for fruit in fruits:
    print(f"- {fruit.capitalize()}")

# ── range() — générer une séquence de nombres ──
for i in range(5):         # 0, 1, 2, 3, 4
    print(i, end=" ")

for i in range(1, 11):     # 1 à 10
    print(i, end=" ")

for i in range(0, 20, 2):  # 0, 2, 4, 6, ..., 18 (step=2)
    print(i, end=" ")

for i in range(10, 0, -1): # 10, 9, 8, ..., 1 (comptage à rebours)
    print(i, end=" ")

# ── enumerate() — index + valeur ──
for index, fruit in enumerate(fruits, start=1):
    print(f"{index}. {fruit}")

# ── zip() — itérer sur plusieurs listes en parallèle ──
noms  = ["Ali", "Sara", "Omar"]
notes = [15, 18, 12]
for nom, note in zip(noms, notes):
    print(f"{nom} : {note}/20")

# ── Itérer sur un dictionnaire ──
personne = {"nom": "Ali", "age": 22, "ville": "Casablanca"}
for cle, valeur in personne.items():
    print(f"  {cle}: {valeur}")

Boucles while

# ── while de base ──
compteur = 0
while compteur < 5:
    print(f"Compteur : {compteur}")
    compteur += 1  # IMPORTANT : toujours avancer pour éviter boucle infinie

# ── Validation d'entrée utilisateur ──
while True:
    reponse = input("Entrez un nombre positif : ")
    if reponse.isdigit() and int(reponse) > 0:
        nombre = int(reponse)
        break  # Sortir de la boucle
    print("Entrée invalide, réessayez.")

# ── Jeu de devinette ──
import random
secret = random.randint(1, 100)
tentatives = 0
while True:
    essai = int(input("Devinez (1-100) : "))
    tentatives += 1
    if essai < secret:   print("Trop petit !")
    elif essai > secret: print("Trop grand !")
    else:
        print(f"Bravo en {tentatives} tentatives !")
        break

Contrôle de Flux : break, continue, pass

# ── break — sortir immédiatement de la boucle ──
for i in range(10):
    if i == 5:
        print("Arrêt à 5 !")
        break
    print(i)

# ── continue — passer à l'itération suivante ──
for i in range(10):
    if i % 2 == 0:  # Ignorer les pairs
        continue
    print(i)  # Affiche 1, 3, 5, 7, 9

# ── pass — instruction vide (placeholder) ──
for i in range(5):
    pass  # TODO : implémenter plus tard

# ── else après une boucle (exécuté si la boucle se termine normalement) ──
for n in range(2, 20):
    for x in range(2, n):
        if n % x == 0:
            break
    else:
        print(f"{n} est un nombre premier")

Compréhensions de Listes (List Comprehensions)

# ── Syntaxe : [expression for élément in séquence if condition] ──

# Carrés de 0 à 9
carres = [x**2 for x in range(10)]
# [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

# Notes >= 10 (filtrées)
notes = [12, 8, 15, 5, 10, 18]
admis = [n for n in notes if n >= 10]
# [12, 15, 10, 18]

# Transformer une liste de chaînes
prenoms = ["ali", "sara", "omar"]
capitalises = [p.capitalize() for p in prenoms]
# ['Ali', 'Sara', 'Omar']

# Compréhension de dictionnaire
carres_dico = {x: x**2 for x in range(1, 6)}
# {1: 1, 2: 4, 3: 9, 4: 16, 5: 25}

# Compréhension d'ensemble
pairs = {x for x in range(10) if x % 2 == 0}
# {0, 2, 4, 6, 8}
Attention : Une boucle while True sans instruction break crée une boucle infinie qui bloque le programme. Assurez-vous toujours d'avoir une condition de sortie !

Question : Que fait [x*2 for x in range(4)] ?

[1, 2, 3, 4]
[0, 2, 4, 6] ✓
[2, 4, 6, 8]
[0, 1, 2, 3]
Section 4

Fonctions — Définition, Portée, Décorateurs et Lambdas

Définir et Appeler des Fonctions

Les fonctions permettent de regrouper du code réutilisable, d'améliorer la lisibilité et d'éviter les répétitions.

# ── Définition de base ──
def saluer(nom):
    """Affiche un message de salutation.
    
    Args:
        nom (str): Le prénom de la personne
    Returns:
        str: Le message de salutation
    """
    message = f"Bonjour, {nom} !"
    return message

# Appel
print(saluer("Omar"))  # "Bonjour, Omar !"

# ── Paramètres par défaut ──
def creer_etudiant(nom, age, ville="Casablanca", actif=True):
    return {"nom": nom, "age": age, "ville": ville, "actif": actif}

e1 = creer_etudiant("Sara", 22)                  # Utilise défauts
e2 = creer_etudiant("Ali", 25, ville="Rabat")    # Arg nommé
e3 = creer_etudiant("Omar", 30, "Fès", False)    # Tout spécifié

Arguments Spéciaux : *args et **kwargs

# ── *args — nombre variable d'arguments positionnels ──
def somme(*nombres):
    """Additionne n'importe quel nombre d'entiers."""
    return sum(nombres)

print(somme(1, 2))          # 3
print(somme(1, 2, 3, 4, 5)) # 15

# ── **kwargs — arguments nommés variables ──
def afficher_infos(**infos):
    for cle, valeur in infos.items():
        print(f"  {cle}: {valeur}")

afficher_infos(nom="Ali", age=25, ville="Casablanca")

# ── Combinaison complète ──
def fonction_complete(obligatoire, *args, defaut="val", **kwargs):
    print(f"Obligatoire: {obligatoire}")
    print(f"Args supplémentaires: {args}")
    print(f"Défaut: {defaut}")
    print(f"Kwargs: {kwargs}")

fonction_complete("A", "B", "C", defaut="D", extra1=1, extra2=2)

# ── Unpacking lors de l'appel ──
nombres = [1, 2, 3]
print(max(*nombres))   # = max(1, 2, 3)

options = {"sep": " | ", "end": "\n"}
print("a", "b", "c", **options)   # a | b | c

Portée des Variables (Scope)

# ── LEGB : Local → Enclosing → Global → Built-in ──

x = "global"

def externe():
    x = "enclosing"

    def interne():
        # nonlocal x  # Pour modifier la variable enclosante
        print(x)  # Accède à "enclosing" (LEGB)

    interne()

externe()

# ── global keyword ──
compteur = 0

def incrementer():
    global compteur   # Accéder à la variable globale
    compteur += 1

incrementer()
incrementer()
print(compteur)  # 2

# ── Bonne pratique : éviter global, préférer le retour de valeur ──
def incrementer_propre(compteur):
    return compteur + 1

c = 0
c = incrementer_propre(c)  # Plus propre !

Fonctions Lambda et Fonctions d'Ordre Supérieur

# ── Lambda — fonction anonyme courte ──
carrer = lambda x: x ** 2
additionner = lambda a, b: a + b
est_pair = lambda n: n % 2 == 0

print(carrer(5))        # 25
print(additionner(3, 4)) # 7

# ── map() — appliquer une fonction à chaque élément ──
nombres = [1, 2, 3, 4, 5]
carres = list(map(lambda x: x**2, nombres))
# [1, 4, 9, 16, 25]

# ── filter() — filtrer selon une condition ──
pairs = list(filter(lambda x: x % 2 == 0, nombres))
# [2, 4]

# ── sorted() avec key ──
etudiants = [
    {"nom": "Sara",  "note": 18},
    {"nom": "Ali",   "note": 12},
    {"nom": "Omar",  "note": 15},
]
# Trier par note (décroissant)
tries = sorted(etudiants, key=lambda e: e["note"], reverse=True)
# [Sara:18, Omar:15, Ali:12]

# ── reduce() — agréger ──
from functools import reduce
produit = reduce(lambda x, y: x * y, [1, 2, 3, 4, 5])  # 120

Décorateurs — Modifier le Comportement des Fonctions

# ── Décorateur simple ──
import time

def mesurer_temps(fonction):
    """Décorateur qui mesure le temps d'exécution."""
    def wrapper(*args, **kwargs):
        debut = time.time()
        resultat = fonction(*args, **kwargs)
        duree = time.time() - debut
        print(f"⏱ {fonction.__name__} : {duree:.4f}s")
        return resultat
    return wrapper

@mesurer_temps
def calcul_lourd(n):
    return sum(i**2 for i in range(n))

calcul_lourd(1_000_000)  # Affiche le temps

# ── Décorateur avec paramètres ──
def repeter(n):
    def decorateur(fonction):
        def wrapper(*args, **kwargs):
            for _ in range(n):
                resultat = fonction(*args, **kwargs)
            return resultat
        return wrapper
    return decorateur

@repeter(3)
def dire_bonjour(nom):
    print(f"Bonjour {nom} !")

dire_bonjour("Ali")  # Affiche 3 fois
Conseil : Documentez vos fonctions avec des docstrings (triple guillemets). L'aide intégrée help(ma_fonction) les affichera automatiquement.

Question : Que retourne list(filter(lambda x: x > 3, [1,2,3,4,5])) ?

[1, 2, 3]
[4, 5] ✓
[3, 4, 5]
[True, True]
Section 5

Structures de Données — Listes, Tuples, Dicts et Sets

Listes (list) — Séquences Mutables

Les listes sont les structures de données les plus utilisées en Python. Elles stockent des éléments ordonnés et modifiables.

# ── Créer une liste ──
vide     = []
nombres  = [1, 2, 3, 4, 5]
mixte    = [42, "Python", True, 3.14, None]
imbriquee = [[1, 2], [3, 4], [5, 6]]

# ── Accès et slicing ──
notes = [12, 15, 8, 17, 10, 14]
print(notes[0])    # 12 (premier)
print(notes[-1])   # 14 (dernier)
print(notes[1:4])  # [15, 8, 17]
print(notes[::2])  # [12, 8, 10] (un sur deux)
print(notes[::-1]) # [14, 10, 17, 8, 15, 12] (inversée)

# ── Méthodes essentielles ──
fruits = ["pomme", "banane", "orange"]
fruits.append("mangue")         # Ajouter à la fin
fruits.insert(1, "fraise")      # Insérer à l'index 1
fruits.extend(["kiwi", "raisin"]) # Fusionner une liste
fruits.remove("banane")         # Supprimer par valeur
del fruits[0]                   # Supprimer par index
popped = fruits.pop()           # Retirer et retourner le dernier
fruits.sort()                   # Trier en place
fruits.sort(reverse=True)       # Trier en ordre décroissant
fruits.reverse()                # Inverser en place
fruits.clear()                  # Vider la liste

# ── Fonctions utiles ──
len(notes)          # 6 — longueur
min(notes)          # 8 — minimum
max(notes)          # 17 — maximum
sum(notes)          # 76 — somme
sorted(notes)       # Nouvelle liste triée
notes.count(15)     # Compter les occurrences
notes.index(17)     # Trouver l'index d'une valeur

Tuples (tuple) — Séquences Immuables

# ── Créer un tuple ──
point      = (3, 5)
couleurs   = ("rouge", "vert", "bleu")
singleton  = (42,)        # Virgule obligatoire pour 1 élément !
sans_paren = 1, 2, 3      # Les parenthèses sont optionnelles

# ── Déballage (unpacking) ──
x, y = point
r, g, b = couleurs
premier, *milieu, dernier = (1, 2, 3, 4, 5)

# ── Utilisation des tuples ──
def min_max(liste):
    return min(liste), max(liste)  # Retourner plusieurs valeurs

mini, maxi = min_max([5, 2, 8, 1, 9, 3])
print(f"Min: {mini}, Max: {maxi}")

# ── Named tuples (tuples avec champs nommés) ──
from collections import namedtuple
Etudiant = namedtuple('Etudiant', ['nom', 'age', 'note'])
e = Etudiant(nom="Ali", age=22, note=15)
print(e.nom, e.note)  # Accès par nom

Dictionnaires (dict) — Paires Clé-Valeur

# ── Créer un dictionnaire ──
etudiant = {
    "nom":   "Sara Benali",
    "age":   22,
    "ville": "Casablanca",
    "notes": [15, 18, 12, 17],
    "actif": True
}

# ── Accès et modification ──
print(etudiant["nom"])                    # Sara Benali
print(etudiant.get("email", "Non renseigné")) # Valeur par défaut

etudiant["email"] = "sara@email.com"      # Ajouter/modifier
etudiant.update({"age": 23, "ville": "Rabat"})  # Plusieurs à la fois
del etudiant["actif"]                     # Supprimer une clé

# ── Méthodes du dictionnaire ──
etudiant.keys()    # dict_keys(['nom', 'age', ...])
etudiant.values()  # dict_values(['Sara', 23, ...])
etudiant.items()   # dict_items([('nom', 'Sara'), ...])
etudiant.pop("email")    # Retirer une clé et retourner sa valeur
etudiant.setdefault("role", "etudiant")  # Ajouter si clé absente

# ── Itération ──
for cle in etudiant:
    print(f"  {cle}: {etudiant[cle]}")

for cle, val in etudiant.items():
    print(f"  {cle} → {val}")

# ── Fusion de dictionnaires (Python 3.9+) ──
d1 = {"a": 1, "b": 2}
d2 = {"b": 3, "c": 4}
fusionne = d1 | d2  # {"a": 1, "b": 3, "c": 4}  — b écrasé par d2

# ── Compréhension ──
carres = {n: n**2 for n in range(1, 6)}
# {1: 1, 2: 4, 3: 9, 4: 16, 5: 25}

Ensembles (set) — Éléments Uniques

# ── Créer un ensemble ──
couleurs_a = {"rouge", "vert", "bleu"}
couleurs_b = {"vert", "bleu", "jaune"}
vide_set   = set()  # {} crée un dict vide, pas un set !

# ── Opérations ensemblistes ──
union        = couleurs_a | couleurs_b  # {"rouge","vert","bleu","jaune"}
intersection = couleurs_a & couleurs_b  # {"vert","bleu"}
difference   = couleurs_a - couleurs_b  # {"rouge"}
sym_diff     = couleurs_a ^ couleurs_b  # {"rouge","jaune"}

# ── Méthodes ──
couleurs_a.add("violet")
couleurs_a.discard("rouge")  # Pas d'erreur si absent
couleurs_a.remove("vert")    # Erreur si absent

# ── Cas d'usage : dédupliquer une liste ──
avec_doublons = [1, 2, 2, 3, 3, 3, 4]
uniques = list(set(avec_doublons))  # [1, 2, 3, 4]

Tableau Comparatif des Structures

StructureSyntaxeMutableOrdonnéDoublon
list[1, 2, 3]✅✅✅
tuple(1, 2, 3)❌✅✅
dict{"k": v}✅✅ (Python 3.7+)❌ (clés)
set{1, 2, 3}✅❌❌

Question : Comment supprimer les doublons d'une liste [1,2,2,3,3] en Python ?

list.unique()
list(set([1,2,2,3,3])) ✓
list.deduplicate()
list.sort(unique=True)
Section 6

Programmation Orientée Objet (POO) en Python

Concepts Fondamentaux de la POO

La Programmation Orientée Objet organise le code en classes (modèles) et objets (instances). C'est le paradigme dominant dans les grands projets logiciels.

Classe Plan ou modèle qui définit les attributs et méthodes d'un objet
Objet Instance d'une classe — une "copie" créée à partir du modèle
Attribut Variable appartenant à un objet (nom, age, solde...)
Méthode Fonction appartenant à une classe (se_connecter, calculer...)
Encapsulation Protéger les données internes d'un objet
Héritage Une classe hérite des attributs et méthodes d'une autre
Polymorphisme Une même méthode se comporte différemment selon la classe

Créer une Classe Complète

class Etudiant:
    """Représente un étudiant dans un centre de formation."""

    # ── Attribut de classe (partagé par tous les objets) ──
    centre = "NB3BN Info"
    nb_etudiants = 0

    def __init__(self, nom, age, email):
        """Constructeur — appelé à la création de l'objet."""
        # Attributs d'instance (spécifiques à chaque objet)
        self.nom   = nom
        self.age   = age
        self.email = email
        self.notes = []
        self._actif = True  # _ = convention "protégé"
        Etudiant.nb_etudiants += 1

    # ── Méthodes d'instance ──
    def ajouter_note(self, matiere, note):
        if 0 <= note <= 20:
            self.notes.append({"matiere": matiere, "note": note})
        else:
            raise ValueError(f"Note invalide : {note}")

    def calculer_moyenne(self):
        if not self.notes:
            return 0
        return sum(n["note"] for n in self.notes) / len(self.notes)

    def afficher_bulletin(self):
        print(f"\n--- Bulletin de {self.nom} ---")
        for n in self.notes:
            print(f"  {n['matiere']:20} : {n['note']:5.1f}/20")
        print(f"  {'Moyenne':20} : {self.calculer_moyenne():5.1f}/20")

    # ── Propriété (getter/setter) ──
    @property
    def actif(self):
        return self._actif

    @actif.setter
    def actif(self, valeur):
        if not isinstance(valeur, bool):
            raise TypeError("actif doit être un booléen")
        self._actif = valeur

    # ── Méthode de classe ──
    @classmethod
    def depuis_dict(cls, data):
        return cls(data["nom"], data["age"], data["email"])

    # ── Méthode statique ──
    @staticmethod
    def est_note_valide(note):
        return 0 <= note <= 20

    # ── Représentations spéciales ──
    def __str__(self):
        return f"Étudiant({self.nom}, {self.age} ans)"

    def __repr__(self):
        return f"Etudiant(nom='{self.nom}', age={self.age})"

    def __len__(self):
        return len(self.notes)


# ── Utilisation ──
e1 = Etudiant("Sara Benali", 22, "sara@email.com")
e2 = Etudiant.depuis_dict({"nom": "Ali", "age": 20, "email": "ali@ex.com"})

e1.ajouter_note("Python", 18)
e1.ajouter_note("JavaScript", 15)
e1.ajouter_note("HTML/CSS", 17)
e1.afficher_bulletin()

print(Etudiant.nb_etudiants)   # 2
print(e1)                       # Étudiant(Sara Benali, 22 ans)
print(len(e1))                  # 3 (nombre de notes)

Héritage et Polymorphisme

class Personne:
    """Classe de base."""
    def __init__(self, nom, age):
        self.nom = nom
        self.age = age

    def se_presenter(self):
        return f"Je suis {self.nom}, {self.age} ans."

class Formateur(Personne):
    """Hérite de Personne."""
    def __init__(self, nom, age, specialite):
        super().__init__(nom, age)  # Appel du constructeur parent
        self.specialite = specialite
        self.formations = []

    def se_presenter(self):  # Override (polymorphisme)
        base = super().se_presenter()
        return f"{base} Je suis formateur en {self.specialite}."

    def creer_formation(self, titre):
        self.formations.append(titre)

class Administrateur(Personne):
    def se_presenter(self):
        return f"{super().se_presenter()} Je suis administrateur."

# ── Polymorphisme ──
equipe = [
    Formateur("Ahmed", 35, "Python"),
    Administrateur("Fatima", 28),
    Etudiant("Omar", 22, "omar@ex.com")
]
for membre in equipe:
    print(membre.se_presenter())

# ── isinstance / issubclass ──
isinstance(e1, Etudiant)  # True
isinstance(e1, Personne)  # True (si Etudiant hérite de Personne)
issubclass(Formateur, Personne)  # True

Classes Abstraites et Interfaces

from abc import ABC, abstractmethod

class FigureGeometrique(ABC):
    """Classe abstraite — ne peut pas être instanciée directement."""

    @abstractmethod
    def aire(self):
        pass

    @abstractmethod
    def perimetre(self):
        pass

    def afficher(self):
        print(f"Aire: {self.aire():.2f}, Périmètre: {self.perimetre():.2f}")

class Cercle(FigureGeometrique):
    import math
    def __init__(self, rayon):
        self.rayon = rayon
    def aire(self):      return 3.14159 * self.rayon ** 2
    def perimetre(self): return 2 * 3.14159 * self.rayon

class Rectangle(FigureGeometrique):
    def __init__(self, l, h):
        self.l, self.h = l, h
    def aire(self):      return self.l * self.h
    def perimetre(self): return 2 * (self.l + self.h)

Cercle(5).afficher()     # Aire: 78.54, Périmètre: 31.42
Rectangle(4, 6).afficher() # Aire: 24.00, Périmètre: 20.00

Question : Quel décorateur transforme une méthode en propriété accessible sans parenthèses ?

@staticmethod
@classmethod
@property ✓
@abstract
Section 7

Fichiers, JSON et Gestion des Exceptions

Lecture et Écriture de Fichiers

Python facilite la manipulation de fichiers texte, CSV, JSON et binaires avec des fonctions intégrées.

# ── Ouvrir et lire un fichier texte ──
# Mode "r" = lecture, "w" = écriture (écrase), "a" = append, "x" = crée nouveau
with open("etudiants.txt", "r", encoding="utf-8") as f:
    contenu_entier = f.read()         # Lire tout
    # OU
    premiereLigne = f.readline()      # Une ligne à la fois
    # OU
    toutes_lignes = f.readlines()     # Liste de lignes

# ── Écrire dans un fichier ──
with open("rapport.txt", "w", encoding="utf-8") as f:
    f.write("=== Rapport NB3BN Info ===\n")
    f.write(f"Date : 2024-09-15\n")
    f.writelines(["Ligne 1\n", "Ligne 2\n"])

# ── Ajouter à la fin ──
with open("log.txt", "a", encoding="utf-8") as f:
    f.write("Nouvelle entrée de log\n")

# ── Parcourir ligne par ligne (mémoire efficace) ──
with open("data.csv", "r", encoding="utf-8") as f:
    for i, ligne in enumerate(f):
        if i == 0:  # Sauter l'en-tête
            continue
        valeurs = ligne.strip().split(",")
        print(valeurs)

Module os et pathlib

import os
from pathlib import Path

# ── os.path (ancien style) ──
os.path.exists("fichier.txt")      # Existe ?
os.path.isfile("fichier.txt")      # Fichier ?
os.path.isdir("dossier/")          # Dossier ?
os.path.basename("/a/b/c.txt")     # "c.txt"
os.path.dirname("/a/b/c.txt")      # "/a/b"
os.path.join("dossier", "fichier") # "dossier/fichier"
os.listdir(".")                    # Liste du répertoire courant
os.makedirs("nouveau/dossier", exist_ok=True)
os.remove("fichier.txt")           # Supprimer un fichier

# ── pathlib (Python 3.6+, style moderne recommandé) ──
p = Path("données/etudiants.txt")
p.exists()          # True/False
p.parent            # Path("données")
p.name              # "etudiants.txt"
p.stem              # "etudiants"
p.suffix            # ".txt"
p.read_text(encoding="utf-8")   # Lire directement
p.write_text("contenu", encoding="utf-8")
list(Path(".").glob("*.py"))     # Tous les fichiers .py
list(Path(".").rglob("*.txt"))   # Récursif

JSON — Sérialisation des Données

import json

# ── Dictionnaire Python → Chaîne JSON ──
data = {
    "centre": "NB3BN Info",
    "etudiants": [
        {"nom": "Sara", "note": 18, "actif": True},
        {"nom": "Ali",  "note": 15, "actif": False}
    ],
    "creé": None
}
json_str = json.dumps(data, ensure_ascii=False, indent=2)
print(json_str)

# ── Chaîne JSON → Dictionnaire Python ──
dictionnaire = json.loads(json_str)
print(dictionnaire["etudiants"][0]["nom"])  # Sara

# ── Écrire du JSON dans un fichier ──
with open("config.json", "w", encoding="utf-8") as f:
    json.dump(data, f, ensure_ascii=False, indent=2)

# ── Lire du JSON depuis un fichier ──
with open("config.json", "r", encoding="utf-8") as f:
    config = json.load(f)

# ── Encoder des types personnalisés ──
from datetime import datetime
class MonEncoder(json.JSONEncoder):
    def default(self, obj):
        if isinstance(obj, datetime):
            return obj.isoformat()
        return super().default(obj)

json.dumps({"date": datetime.now()}, cls=MonEncoder)

Gestion des Exceptions

# ── Structure try / except / else / finally ──
try:
    # Code susceptible de lever une exception
    fichier = open("data.txt", "r")
    nombre = int(input("Nombre : "))
    resultat = 100 / nombre

except FileNotFoundError as e:
    print(f"Fichier introuvable : {e.filename}")

except ValueError:
    print("Ce n'est pas un entier valide !")

except ZeroDivisionError:
    print("Division par zéro interdite !")

except (TypeError, AttributeError) as e:
    print(f"Erreur de type ou d'attribut : {e}")

except Exception as e:
    # Attraper toute autre exception
    print(f"Erreur inattendue : {type(e).__name__}: {e}")

else:
    # Exécuté seulement si aucune exception n'est levée
    print(f"Résultat : {resultat}")
    fichier.close()

finally:
    # Toujours exécuté, erreur ou non (nettoyage)
    print("Fin du traitement.")

Créer ses Propres Exceptions

class ErreurValidation(Exception):
    """Levée quand la validation d'une donnée échoue."""
    def __init__(self, champ, valeur, message):
        self.champ = champ
        self.valeur = valeur
        super().__init__(f"Champ '{champ}' invalide ({valeur}): {message}")

class ErreurAuthentification(Exception):
    """Levée lors d'un échec de connexion."""
    pass

def creer_etudiant(nom, age):
    if not nom or not nom.strip():
        raise ErreurValidation("nom", nom, "Le nom ne peut pas être vide")
    if not isinstance(age, int) or age < 5 or age > 120:
        raise ErreurValidation("age", age, "L'âge doit être entre 5 et 120")
    return {"nom": nom.strip(), "age": age}

try:
    e = creer_etudiant("", 25)
except ErreurValidation as err:
    print(f"❌ {err}")     # Champ 'nom' invalide (): Le nom ne peut pas être vide
Important : Utilisez toujours with open(...) as f: pour ouvrir les fichiers. Cela garantit la fermeture automatique même en cas d'erreur.

Question : Quelle fonction convertit un dictionnaire Python en chaîne JSON ?

json.load()
json.dumps() ✓
json.encode()
json.stringify()
Section 8

Modules et Bibliothèques Standard Python

Le Système de Modules Python

Python est livré avec une bibliothèque standard très riche. Des centaines de modules sont disponibles sans installation supplémentaire.

Module datetime — Dates et Heures

from datetime import datetime, date, timedelta
import locale

# ── Date et heure actuelles ──
maintenant  = datetime.now()
aujourd_hui = date.today()
print(maintenant)               # 2024-09-15 14:30:25.123456

# ── Créer une date spécifique ──
naissance = date(2000, 3, 25)   # 25 mars 2000
evenement = datetime(2024, 1, 1, 9, 0, 0)  # 1er jan 2024 à 9h

# ── Formatage ──
print(maintenant.strftime("%d/%m/%Y %H:%M"))  # 15/09/2024 14:30
print(maintenant.strftime("%A %d %B %Y"))      # Sunday 15 September 2024

# ── Parser une chaîne ──
dt = datetime.strptime("15/09/2024 14:30", "%d/%m/%Y %H:%M")

# ── Arithmétique ──
dans_7_jours  = aujourd_hui + timedelta(days=7)
il_y_a_1_mois = aujourd_hui - timedelta(days=30)
age = (date.today() - naissance).days // 365

# ── Comparer des dates ──
if date.today() > naissance:
    print("C'est le passé")

Module random — Nombres Aléatoires

import random

random.random()             # Float entre 0.0 et 1.0
random.randint(1, 100)      # Entier entre 1 et 100 inclus
random.uniform(1.5, 5.5)    # Float entre 1.5 et 5.5
random.choice(["a","b","c"]) # Choisir un élément aléatoire
random.sample(range(50), 6)  # 6 éléments uniques (loterie)

notes = [10, 12, 15, 18]
random.shuffle(notes)        # Mélanger en place

# ── Seed pour résultats reproductibles ──
random.seed(42)
print(random.randint(1, 100))  # Toujours le même résultat avec seed=42

Module re — Expressions Régulières

import re

# ── Correspondance (match/search) ──
email = "contact@nb3bn.ma"
if re.match(r'^[w.+-]+@[w-]+.[a-zA-Z]{2,}$', email):
    print("Email valide")

# ── Trouver toutes les occurrences ──
texte = "Prix: 1500 MAD, Réduction: 200 MAD, Total: 1300 MAD"
prix  = re.findall(r'd+', texte)   # ['1500', '200', '1300']

# ── Remplacer ──
propre = re.sub(r's+', ' ', "trop    d'espaces   ici")  # "trop d'espaces ici"
digits = re.sub(r'D', '', "Tél: 06-12-34-56-78")  # "0612345678"

# ── Groupes de capture ──
date_str = "2024-09-15"
m = re.match(r'(d{4})-(d{2})-(d{2})', date_str)
if m:
    annee, mois, jour = m.group(1), m.group(2), m.group(3)

Module collections — Structures Avancées

from collections import Counter, defaultdict, deque, OrderedDict

# ── Counter — Compter les occurrences ──
mots = ["python", "java", "python", "c++", "python", "java"]
comptage = Counter(mots)
print(comptage)           # Counter({'python': 3, 'java': 2, 'c++': 1})
print(comptage.most_common(2))  # [('python', 3), ('java', 2)]

# ── defaultdict — Dictionnaire avec valeur par défaut ──
groupes = defaultdict(list)
for etudiant, niveau in [("Ali","A"), ("Sara","B"), ("Omar","A")]:
    groupes[niveau].append(etudiant)
# {'A': ['Ali', 'Omar'], 'B': ['Sara']}

# ── deque — File double (rapide aux deux extrémités) ──
file = deque(maxlen=5)  # File circulaire de taille 5
file.append("msg1")
file.appendleft("urgent")
file.popleft()
file.pop()

Module itertools — Itérateurs Avancés

import itertools

# ── Produit cartésien ──
for combo in itertools.product([1,2], ['a','b']):
    print(combo)  # (1,'a'), (1,'b'), (2,'a'), (2,'b')

# ── Permutations et combinaisons ──
list(itertools.permutations("ABC", 2))  # [('A','B'),('A','C'),('B','A'),...]
list(itertools.combinations("ABCD", 3)) # [('A','B','C'),...]

# ── groupby — Grouper des données ──
data = [("Maths", 15), ("Python", 18), ("Maths", 12), ("Python", 16)]
data.sort(key=lambda x: x[0])
for matiere, notes in itertools.groupby(data, key=lambda x: x[0]):
    print(f"{matiere}: {list(n[1] for n in notes)}")

Module logging — Journalisation Professionnelle

import logging

# ── Configuration basique ──
logging.basicConfig(
    level=logging.DEBUG,
    format='%(asctime)s [%(levelname)s] %(name)s: %(message)s',
    handlers=[
        logging.FileHandler("app.log", encoding="utf-8"),
        logging.StreamHandler()  # Console aussi
    ]
)
logger = logging.getLogger("nb3bn")

logger.debug("Détails techniques")
logger.info("Application démarrée")
logger.warning("Fichier de config manquant, utilisation des défauts")
logger.error("Erreur de connexion à la base de données")
logger.critical("Arrêt immédiat requis !")

Question : Quelle classe collections permet de compter les occurrences d'éléments ?

defaultdict
Counter ✓
deque
namedtuple
Section 9

NumPy, Pandas et Requests — Bibliothèques Populaires

NumPy — Calcul Numérique et Tableaux

NumPy (Numerical Python) est la bibliothèque fondamentale pour le calcul scientifique en Python. Elle fournit des tableaux multidimensionnels ultra-rapides.

import numpy as np

# ── Créer des tableaux ──
arr1D  = np.array([1, 2, 3, 4, 5])
arr2D  = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
zeros  = np.zeros((3, 4))         # Matrice 3×4 de zéros
uns    = np.ones((2, 5))          # Matrice 2×5 de 1
range_ = np.arange(0, 20, 2)     # [0,2,4,...,18]
linsp  = np.linspace(0, 1, 5)    # [0, 0.25, 0.5, 0.75, 1]
alea   = np.random.rand(3, 3)    # Matrice 3×3 valeurs aléatoires [0,1)

# ── Opérations vectorisées (rapides !) ──
a = np.array([1, 2, 3, 4])
b = np.array([10, 20, 30, 40])
print(a + b)    # [11, 22, 33, 44]
print(a * 2)    # [2, 4, 6, 8]
print(a ** 2)   # [1, 4, 9, 16]
print(np.sqrt(a)) # [1., 1.414, 1.732, 2.]

# ── Statistiques ──
notes = np.array([12, 15, 8, 17, 10, 14, 18, 9])
print(f"Moyenne : {np.mean(notes):.2f}")       # 12.88
print(f"Écart-type : {np.std(notes):.2f}")     # 3.40
print(f"Médiane : {np.median(notes):.1f}")     # 13.0
print(f"Min/Max : {np.min(notes)}/{np.max(notes)}")

# ── Indexation et filtrage ──
arr = np.array([5, 12, 3, 8, 15, 7])
print(arr[arr > 8])           # [12, 15] — filtrage booléen
print(arr[[0, 2, 4]])         # [5, 3, 15] — indexation fancy
arr[arr < 5] = 0              # Modifier en masse

Pandas — Analyse de Données

import pandas as pd

# ── Créer un DataFrame ──
data = {
    "Nom":     ["Ali", "Sara", "Omar", "Fatima", "Ahmed"],
    "Age":     [22, 25, 20, 28, 23],
    "Ville":   ["Casablanca", "Rabat", "Fès", "Casablanca", "Marrakech"],
    "Note":    [15, 18, 12, 17, 14],
    "Admis":   [True, True, True, True, True]
}
df = pd.DataFrame(data)

# ── Explorer le DataFrame ──
print(df.head(3))     # 3 premières lignes
print(df.tail(2))     # 2 dernières lignes
print(df.info())      # Types et valeurs manquantes
print(df.describe())  # Statistiques descriptives
print(df.shape)       # (5, 5)

# ── Sélection ──
print(df["Nom"])                      # Colonne
print(df[["Nom", "Note"]])            # Plusieurs colonnes
print(df.iloc[0])                     # Première ligne (index)
print(df.loc[df["Ville"] == "Casablanca"])  # Filtrer

# ── Filtrage avancé ──
bons = df[(df["Note"] >= 15) & (df["Admis"] == True)]
# Filtre : note >= 15 ET admis

# ── Agrégations ──
print(df["Note"].mean())      # Moyenne
print(df.groupby("Ville")["Note"].mean())  # Moyenne par ville

# ── Lire / écrire CSV ──
df.to_csv("etudiants.csv", index=False, encoding="utf-8")
df_lu = pd.read_csv("etudiants.csv")

# ── Lire / écrire Excel ──
df.to_excel("etudiants.xlsx", index=False)
df_xl = pd.read_excel("etudiants.xlsx")

# ── Traiter les valeurs manquantes ──
df["Email"] = None  # Ajouter colonne vide
df["Email"].fillna("inconnu@email.com", inplace=True)
df.dropna(inplace=True)  # Supprimer les lignes avec NaN

Requests — HTTP et APIs

import requests

# ── GET simple ──
response = requests.get("https://api.nb3bn.ma/formations")
print(response.status_code)  # 200
data = response.json()       # Dictionnaire Python
print(data)

# ── GET avec paramètres ──
params = {"niveau": "débutant", "page": 1, "limit": 10}
r = requests.get("https://api.nb3bn.ma/formations", params=params)
# URL = .../formations?niveau=débutant&page=1&limit=10

# ── POST avec données JSON ──
nouvelle_formation = {
    "titre": "Python Avancé",
    "prix": 1500,
    "niveau": "avancé"
}
r = requests.post(
    "https://api.nb3bn.ma/formations",
    json=nouvelle_formation,
    headers={"Authorization": "Bearer MON_TOKEN_SECRET"}
)
print(r.status_code, r.json())

# ── Gestion des erreurs ──
try:
    r = requests.get("https://api.example.com/data", timeout=5)
    r.raise_for_status()  # Lève une exception si 4xx ou 5xx
    return r.json()
except requests.exceptions.Timeout:
    print("Délai dépassé")
except requests.exceptions.HTTPError as e:
    print(f"Erreur HTTP {e.response.status_code}")
except requests.exceptions.ConnectionError:
    print("Pas de connexion internet")

BeautifulSoup — Web Scraping

from bs4 import BeautifulSoup
import requests

# ── Récupérer et parser une page web ──
r = requests.get("https://example.com", headers={"User-Agent": "Mozilla/5.0"})
soup = BeautifulSoup(r.content, "html.parser")

# ── Extraire du contenu ──
titre = soup.find("h1").text               # Premier h1
liens = [a["href"] for a in soup.find_all("a") if a.get("href")]
images = [img["src"] for img in soup.find_all("img")]

# ── Sélecteurs CSS ──
cartes = soup.select(".card")
premier_titre = soup.select_one("#main h2").text

Question : Quelle méthode Pandas regroupe les données par colonne ?

df.sort_by()
df.groupby() ✓
df.filter()
df.aggregate()
Section 10

Automatisation et Scripts Pratiques

Python pour l'Automatisation

L'un des usages les plus populaires de Python est l'automatisation de tâches répétitives : traitement de fichiers, envoi d'emails, manipulation de données, etc.

Traitement de Fichiers CSV en Masse

import csv
import os
from pathlib import Path

# ── Lire un CSV ──
with open("notes.csv", "r", encoding="utf-8", newline="") as f:
    lecteur = csv.DictReader(f)  # Chaque ligne = dictionnaire
    etudiants = list(lecteur)

# Traiter les données
for e in etudiants:
    note = float(e["note"])
    e["mention"]  = "Admis" if note >= 10 else "Refusé"
    e["moyenne"]  = f"{note:.1f}"

# ── Écrire un CSV ──
champs = ["nom", "email", "note", "mention"]
with open("resultats.csv", "w", encoding="utf-8", newline="") as f:
    ecrivain = csv.DictWriter(f, fieldnames=champs, extrasaction="ignore")
    ecrivain.writeheader()
    ecrivain.writerows(etudiants)

# ── Traiter tous les CSV d'un dossier ──
dossier = Path("./données")
for fichier in dossier.glob("*.csv"):
    print(f"Traitement de {fichier.name}...")
    # ... traitement
    nouveau_nom = fichier.stem + "_traité" + fichier.suffix
    fichier.rename(fichier.parent / nouveau_nom)

Envoi d'Emails Automatique

import smtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from email.mime.base import MIMEBase
from email import encoders

def envoyer_email(destinataire, sujet, corps_html, piece_jointe=None):
    """Envoie un email HTML avec pièce jointe optionnelle."""
    
    SMTP_SERVER   = "smtp.gmail.com"
    SMTP_PORT     = 587
    EMAIL_ENVOYEUR = "nb3bn@gmail.com"
    MOT_DE_PASSE  = "mdp_app_gmail"  # Utiliser App Password Gmail
    
    msg = MIMEMultipart("alternative")
    msg["Subject"] = sujet
    msg["From"]    = EMAIL_ENVOYEUR
    msg["To"]      = destinataire
    
    # Corps HTML
    msg.attach(MIMEText(corps_html, "html", "utf-8"))
    
    # Pièce jointe (si fournie)
    if piece_jointe:
        with open(piece_jointe, "rb") as f:
            part = MIMEBase("application", "octet-stream")
            part.set_payload(f.read())
            encoders.encode_base64(part)
            part.add_header("Content-Disposition", f"attachment; filename={piece_jointe}")
            msg.attach(part)
    
    with smtplib.SMTP(SMTP_SERVER, SMTP_PORT) as serveur:
        serveur.starttls()
        serveur.login(EMAIL_ENVOYEUR, MOT_DE_PASSE)
        serveur.sendmail(EMAIL_ENVOYEUR, destinataire, msg.as_string())
        print(f"✅ Email envoyé à {destinataire}")

# ── Envoi en masse ──
with open("etudiants.csv", newline="") as f:
    for row in csv.DictReader(f):
        corps = f"""
        <h2>Résultats de l'examen — NB3BN Info</h2>
        <p>Bonjour <strong>{row['nom']}</strong>,</p>
        <p>Votre note : <strong>{row['note']}/20</strong></p>
        """
        envoyer_email(row["email"], "Vos résultats NB3BN Info", corps)

Automatisation Web avec Selenium

from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
import time

# ── Configurer Chrome ──
options = webdriver.ChromeOptions()
options.add_argument("--headless")  # Mode invisible
driver = webdriver.Chrome(options=options)

try:
    # ── Naviguer ──
    driver.get("https://nb3bn.ma/login")
    
    # ── Remplir le formulaire ──
    driver.find_element(By.ID, "email").send_keys("admin@nb3bn.ma")
    driver.find_element(By.ID, "password").send_keys("MonMotDePasse")
    
    # ── Cliquer un bouton ──
    driver.find_element(By.CSS_SELECTOR, "button[type='submit']").click()
    
    # ── Attendre un élément ──
    WebDriverWait(driver, 10).until(
        EC.presence_of_element_located((By.ID, "dashboard"))
    )
    
    # ── Prendre une capture d'écran ──
    driver.save_screenshot("dashboard.png")
    
    # ── Extraire du texte ──
    titre = driver.find_element(By.TAG_NAME, "h1").text
    print(f"Titre : {titre}")

finally:
    driver.quit()

Planification de Tâches avec schedule

import schedule
import time
from datetime import datetime

def rapport_quotidien():
    """Générer et envoyer le rapport journalier."""
    print(f"[{datetime.now():%H:%M}] Génération du rapport...")
    # ... logique de rapport
    # envoyer_email(...)
    print("✅ Rapport envoyé !")

def nettoyage_hebdomadaire():
    """Nettoyer les fichiers temporaires."""
    print("Nettoyage des fichiers temporaires...")

def verifier_api():
    """Vérifier que l'API est disponible."""
    import requests
    try:
        r = requests.get("https://api.nb3bn.ma/health", timeout=3)
        if r.status_code != 200:
            print("⚠️ API indisponible !")
    except:
        print("❌ API inaccessible !")

# ── Programmer les tâches ──
schedule.every().day.at("08:00").do(rapport_quotidien)
schedule.every().monday.at("03:00").do(nettoyage_hebdomadaire)
schedule.every(5).minutes.do(verifier_api)

# ── Boucle principale ──
print("✅ Planificateur démarré. Ctrl+C pour arrêter.")
while True:
    schedule.run_pending()
    time.sleep(60)

Créer une API avec Flask

from flask import Flask, request, jsonify
import json

app = Flask(__name__)

# Données en mémoire (utiliser une BD en production)
formations = [
    {"id": 1, "titre": "Python", "prix": 1500},
    {"id": 2, "titre": "JavaScript", "prix": 2000},
]

@app.route('/api/formations', methods=['GET'])
def get_formations():
    return jsonify({"success": True, "data": formations})

@app.route('/api/formations', methods=['POST'])
def creer_formation():
    data = request.get_json()
    nouvelle = {"id": len(formations)+1, **data}
    formations.append(nouvelle)
    return jsonify({"success": True, "data": nouvelle}), 201

if __name__ == '__main__':
    app.run(debug=True, port=5000)

Question : Quelle bibliothèque Python permet d'automatiser un navigateur web ?

requests
BeautifulSoup
Selenium ✓
urllib
Section 11

Tests, Débogage et Bonnes Pratiques Python

Pourquoi Tester son Code ?

Les tests automatisés garantissent que votre code fonctionne correctement, même après des modifications. Ils évitent les régressions et documentent le comportement attendu.

Types de tests :
🔬 Tests unitaires — Tester une fonction isolée
🔗 Tests d'intégration — Tester plusieurs composants ensemble
🌐 Tests end-to-end (E2E) — Tester l'application complète

unittest — Framework de Test Intégré

import unittest

# ── Fichier : calculatrice.py ──
def additionner(a, b):   return a + b
def diviser(a, b):
    if b == 0: raise ValueError("Division par zéro")
    return a / b
def moyenne(notes):
    if not notes: return 0
    return sum(notes) / len(notes)

# ── Fichier : test_calculatrice.py ──
class TestCalculatrice(unittest.TestCase):

    def setUp(self):
        """Appelé avant chaque test."""
        self.notes = [10, 12, 15, 18]

    def tearDown(self):
        """Appelé après chaque test."""
        pass

    # ── Tests d'additionner ──
    def test_addition_positive(self):
        self.assertEqual(additionner(3, 5), 8)

    def test_addition_negative(self):
        self.assertEqual(additionner(-3, 5), 2)

    def test_addition_zero(self):
        self.assertEqual(additionner(0, 0), 0)

    # ── Tests de diviser ──
    def test_division_normale(self):
        self.assertAlmostEqual(diviser(10, 3), 3.333, places=2)

    def test_division_par_zero(self):
        with self.assertRaises(ValueError):
            diviser(10, 0)

    # ── Tests de moyenne ──
    def test_moyenne_normale(self):
        self.assertEqual(moyenne(self.notes), 13.75)

    def test_moyenne_liste_vide(self):
        self.assertEqual(moyenne([]), 0)

    def test_assertion_types(self):
        self.assertIsInstance(moyenne([10, 20]), float)
        self.assertIn(15, self.notes)
        self.assertTrue(len(self.notes) > 0)

if __name__ == '__main__':
    unittest.main(verbosity=2)

pytest — Framework Moderne

# Installation : pip install pytest pytest-cov

# ── test_formations.py ──
import pytest

# ── Fixtures ──
@pytest.fixture
def liste_formations():
    return [
        {"id": 1, "titre": "Python", "prix": 1500, "actif": True},
        {"id": 2, "titre": "JS",     "prix": 2000, "actif": False},
    ]

# ── Tests simples ──
def test_longueur_formations(liste_formations):
    assert len(liste_formations) == 2

def test_premiere_formation(liste_formations):
    assert liste_formations[0]["titre"] == "Python"
    assert liste_formations[0]["prix"]  == 1500

# ── Tests paramétrés ──
@pytest.mark.parametrize("note,attendu", [
    (18, "Très Bien"),
    (15, "Bien"),
    (12, "Passable"),
    (9,  "Insuffisant"),
])
def test_mention(note, attendu):
    from mon_module import get_mention
    assert get_mention(note) == attendu

# ── Exécuter les tests ──
# pytest -v                    # Verbose
# pytest --cov=mon_module      # Couverture de code

Débogage avec pdb

import pdb

def calcul_complexe(donnees):
    resultat = 0
    for item in donnees:
        pdb.set_trace()  # Point d'arrêt — ouvre le débogueur interactif
        # Commandes pdb :
        #   n = next (pas à pas)
        #   s = step into (entrer dans une fonction)
        #   c = continue (reprendre)
        #   p variable = afficher une variable
        #   l = list (voir le code)
        #   q = quit
        resultat += item["valeur"] * item["poids"]
    return resultat

# ── Avec Python 3.7+ : breakpoint() ──
def ma_fonction():
    x = 10
    breakpoint()  # Équivalent à pdb.set_trace()
    return x * 2

Profiling — Optimiser les Performances

import cProfile
import timeit

# ── Mesurer le temps d'exécution ──
temps = timeit.timeit("'-'.join(str(n) for n in range(100))", number=10000)
print(f"Temps : {temps:.3f}s")

# ── Comparer deux implémentations ──
def lent(n):   return [str(i) for i in range(n)]
def rapide(n): return list(map(str, range(n)))

print(timeit.timeit(lambda: lent(1000), number=1000))
print(timeit.timeit(lambda: rapide(1000), number=1000))

# ── Profiler une fonction complète ──
cProfile.run('ma_fonction_lente()', sort='cumulative')

Style de Code — PEP 8 et Bonnes Pratiques

# ✅ Bon style PEP 8
CONSTANTE = 42
variable_locale = "valeur"

def nom_de_fonction_clair(parametre_un, parametre_deux=None):
    """Docstring obligatoire pour les fonctions publiques."""
    if parametre_deux is None:
        parametre_deux = []
    return parametre_un + len(parametre_deux)

class NomDeClasseEnPascalCase:
    def methode_instance(self):
        pass

# ✅ Formatage
x = 1 + 2                  # Espaces autour des opérateurs
liste = [1, 2, 3]           # Espace après virgule
y = {"cle": "valeur"}       # Espace après :

# ✅ Lignes longues — max 79 caractères (PEP 8) ou 119 (pratique moderne)
resultat = (
    premiere_partie
    + deuxieme_partie
    + troisieme_partie
)

# ── Linting automatique ──
# pip install black flake8 pylint
# black mon_fichier.py        # Auto-formatage
# flake8 mon_fichier.py       # Vérification style
Conseil : Installez l'extension Pylance dans VS Code pour avoir la complétion automatique, la vérification de types et les suggestions de style en temps réel.

Question : Quel nom de convention Python est utilisé pour les noms de variables et fonctions ?

camelCase
snake_case ✓
PascalCase
UPPER_CASE
Section 12

Projet Final — Application Python Complète

Projet : Système de Gestion des Notes NB3BN Info

Nous allons construire une application Python complète avec interface en ligne de commande, fichiers JSON pour la persistance, et fonctionnalités d'export CSV/Email.

Architecture du Projet

nb3bn_gestion/
├── main.py              # Point d'entrée — menu principal
├── models/
│   ├── __init__.py
│   ├── etudiant.py      # Classe Etudiant
│   ├── formation.py     # Classe Formation
│   └── inscription.py  # Classe Inscription
├── services/
│   ├── __init__.py
│   ├── stockage.py      # Lecture/écriture JSON
│   ├── rapports.py      # Génération de rapports CSV
│   └── email_service.py # Envoi d'emails
├── data/
│   ├── etudiants.json
│   └── formations.json
├── exports/             # Dossier pour les rapports
├── requirements.txt
└── tests/
    ├── test_etudiant.py
    └── test_stockage.py

Étape 1 : Modèle de Données

# models/etudiant.py
from dataclasses import dataclass, field, asdict
from datetime import date
from typing import Optional

@dataclass
class Note:
    matiere: str
    note: float
    date: str = field(default_factory=lambda: date.today().isoformat())

    def __post_init__(self):
        if not (0 <= self.note <= 20):
            raise ValueError(f"Note invalide : {self.note}")

@dataclass
class Etudiant:
    nom:     str
    email:   str
    age:     int
    niveau:  str = "débutant"
    notes:   list = field(default_factory=list)
    id:      int  = field(default=0)
    actif:   bool = True

    def __post_init__(self):
        if not self.nom.strip():
            raise ValueError("Le nom est requis")
        if "@" not in self.email:
            raise ValueError("Email invalide")
        if not (15 <= self.age <= 80):
            raise ValueError("Âge invalide")

    def ajouter_note(self, matiere: str, valeur: float):
        self.notes.append(Note(matiere, valeur))

    @property
    def moyenne(self) -> float:
        if not self.notes: return 0.0
        return round(sum(n.note for n in self.notes) / len(self.notes), 2)

    @property
    def mention(self) -> str:
        m = self.moyenne
        if m >= 18: return "Très Bien"
        if m >= 16: return "Bien"
        if m >= 14: return "Assez Bien"
        if m >= 10: return "Passable"
        return "Insuffisant"

    def to_dict(self) -> dict:
        d = asdict(self)
        d["moyenne"] = self.moyenne
        d["mention"] = self.mention
        return d

Étape 2 : Service de Stockage

# services/stockage.py
import json
from pathlib import Path
from models.etudiant import Etudiant, Note

DOSSIER = Path("data")
DOSSIER.mkdir(exist_ok=True)

class StockageEtudiants:
    def __init__(self):
        self.fichier = DOSSIER / "etudiants.json"
        self._etudiants: list[Etudiant] = []
        self._prochain_id = 1
        self.charger()

    def charger(self):
        if self.fichier.exists():
            with open(self.fichier, encoding="utf-8") as f:
                data = json.load(f)
                self._etudiants = []
                for d in data:
                    e = Etudiant(
                        nom=d["nom"], email=d["email"], age=d["age"],
                        niveau=d.get("niveau", "débutant"),
                        id=d.get("id", 0), actif=d.get("actif", True)
                    )
                    e.notes = [Note(n["matiere"], n["note"], n["date"])
                               for n in d.get("notes", [])]
                    self._etudiants.append(e)
                self._prochain_id = max((e.id for e in self._etudiants), default=0) + 1

    def sauvegarder(self):
        with open(self.fichier, "w", encoding="utf-8") as f:
            json.dump([e.to_dict() for e in self._etudiants], f,
                      ensure_ascii=False, indent=2)

    def ajouter(self, etudiant: Etudiant) -> Etudiant:
        etudiant.id = self._prochain_id
        self._prochain_id += 1
        self._etudiants.append(etudiant)
        self.sauvegarder()
        return etudiant

    def trouver_par_email(self, email: str) -> Etudiant | None:
        return next((e for e in self._etudiants if e.email == email), None)

    def lister(self, actif_seulement=True) -> list[Etudiant]:
        if actif_seulement:
            return [e for e in self._etudiants if e.actif]
        return self._etudiants.copy()

Étape 3 : Interface Principale (CLI)

# main.py
from services.stockage import StockageEtudiants
from models.etudiant import Etudiant
import csv

storage = StockageEtudiants()

def afficher_menu():
    print("\n" + "="*40)
    print("    NB3BN Info — Gestion des Notes")
    print("="*40)
    print("1. 📋 Lister les étudiants")
    print("2. ➕ Ajouter un étudiant")
    print("3. 📝 Saisir des notes")
    print("4. 📊 Afficher le bulletin")
    print("5. 📤 Exporter en CSV")
    print("6. 🔍 Rechercher un étudiant")
    print("0. ❌ Quitter")
    print("="*40)

def lister_etudiants():
    etudiants = storage.lister()
    if not etudiants:
        print("  ℹ️ Aucun étudiant enregistré.")
        return
    print(f"\n{'#':4} {'Nom':<20} {'Email':<25} {'Moy':>6} {'Mention':<12}")
    print("-" * 75)
    for e in sorted(etudiants, key=lambda x: x.moyenne, reverse=True):
        print(f"  {e.id:<3} {e.nom:<20} {e.email:<25} {e.moyenne:>6.1f} {e.mention}")

def ajouter_etudiant():
    print("\n--- Ajouter un étudiant ---")
    try:
        nom   = input("Nom complet : ").strip()
        email = input("Email : ").strip()
        age   = int(input("Âge : "))
        e = Etudiant(nom=nom, email=email, age=age)
        storage.ajouter(e)
        print(f"✅ {nom} ajouté avec succès (ID: {e.id})")
    except (ValueError, KeyboardInterrupt) as err:
        print(f"❌ Erreur : {err}")

def exporter_csv():
    etudiants = storage.lister()
    fichier = "exports/bulletin_global.csv"
    Path("exports").mkdir(exist_ok=True)
    with open(fichier, "w", newline="", encoding="utf-8") as f:
        w = csv.writer(f)
        w.writerow(["ID", "Nom", "Email", "Âge", "Moyenne", "Mention"])
        for e in etudiants:
            w.writerow([e.id, e.nom, e.email, e.age, e.moyenne, e.mention])
    print(f"✅ Exporté : {fichier}")

def main():
    print("Bienvenue dans le Système NB3BN Info !")
    while True:
        afficher_menu()
        choix = input("Votre choix : ").strip()
        actions = {
            "1": lister_etudiants,
            "2": ajouter_etudiant,
            "5": exporter_csv,
        }
        if choix == "0":
            print("Au revoir !")
            break
        elif choix in actions:
            actions[choix]()
        else:
            print("❌ Choix invalide")

if __name__ == "__main__":
    main()

Étape 4 : Tests Automatisés

# tests/test_etudiant.py
import pytest
from models.etudiant import Etudiant, Note

class TestEtudiant:
    def test_creation_valide(self):
        e = Etudiant("Sara", "sara@test.com", 22)
        assert e.nom == "Sara"
        assert e.moyenne == 0.0

    def test_nom_vide_interdit(self):
        with pytest.raises(ValueError, match="nom"):
            Etudiant("", "a@b.com", 22)

    def test_email_invalide(self):
        with pytest.raises(ValueError, match="mail"):
            Etudiant("Ali", "pas_un_email", 22)

    def test_moyenne_calculee(self):
        e = Etudiant("Omar", "o@t.com", 20)
        e.ajouter_note("Python", 18)
        e.ajouter_note("JS", 14)
        assert e.moyenne == 16.0
        assert e.mention  == "Bien"

    def test_mention_tres_bien(self):
        e = Etudiant("Fatima", "f@t.com", 25)
        e.ajouter_note("Python", 19)
        e.ajouter_note("React", 18)
        assert e.mention == "Très Bien"

Récapitulatif des Compétences Python Acquises

SujetNiveauConcepts clés
Bases du langage⭐⭐⭐⭐⭐Variables, types, opérateurs, f-strings
Structures de contrôle⭐⭐⭐⭐⭐if/elif/else, for, while, comprehensions
Fonctions⭐⭐⭐⭐⭐args/kwargs, lambda, décorateurs
Structures de données⭐⭐⭐⭐⭐list, dict, tuple, set
POO⭐⭐⭐⭐☆Classes, héritage, décorateurs, dataclass
Fichiers & JSON⭐⭐⭐⭐⭐open/with, json.load/dumps, pathlib
Modules standard⭐⭐⭐⭐☆datetime, random, re, collections
Bibliothèques tierces⭐⭐⭐⭐☆NumPy, Pandas, requests, Flask
Automatisation⭐⭐⭐⭐☆CSV, email, Selenium, schedule
Tests⭐⭐⭐⭐☆unittest, pytest, coverage
Prochaines étapes : Approfondissez avec Django ou FastAPI pour le développement web, scikit-learn et TensorFlow pour le machine learning, et asyncio pour la programmation asynchrone avancée.

Question finale : Quel décorateur Python 3.7+ remplace le __init__ boilerplate dans les classes de données ?

@property
@staticmethod
@dataclass ✓
@classmethod

Félicitations !

Vous avez terminé ce manuel de formation. Passez maintenant votre test de niveau sur NB3BN Info.

Passer le Test de Niveau