Files
TermNSI/Revisions_Rentree/_generateur.py
T

1095 lines
34 KiB
Python

# -*- coding: utf-8 -*-
"""Génère les deux notebooks PERRIAND-7 (élève + corrigé)."""
import json, os, uuid
CELLS = []
def md(src):
CELLS.append(("markdown", src, src))
def code(eleve, corrige=None):
CELLS.append(("code", eleve, corrige if corrige is not None else eleve))
# ---------------------------------------------------------------- INTRO
md("""# PERRIAND-7 : protocole de réveil
> **Journal de bord, jour 68.**
> Il y a soixante-huit jours, l'équipage de la station orbitale **PERRIAND-7** est entré
> en cryosommeil pour traverser la ceinture de radiations de Van Allen étendue.
> Vous venez d'ouvrir les yeux.
>
> La station, elle, s'est moins bien réveillée que vous. Les radiations ont provoqué des
> corruptions mémoire dans l'ordinateur de bord : plusieurs programmes affichent des
> valeurs absurdes, d'autres refusent tout simplement de s'exécuter.
>
> Vous êtes l'**ingénieur système** de bord. Cinq modules à réveiller, puis la sortie
> d'orbite. Dans l'ordre.
---
## Comment travailler
Chaque module contient quatre types d'exercices :
| Type | Ce qu'on attend de vous |
|---|---|
| **DIAGNOSTIC** | Lire le code et **prédire** ce qu'il affiche **avant** de l'exécuter. Écrivez votre prédiction dans le commentaire prévu, puis exécutez pour vérifier. |
| **RÉPARATION** | Le code est cassé. Trouvez l'erreur et corrigez-la. |
| **PRODUCTION** | Écrivez le code demandé. |
| **BONUS** | Facultatif. À faire si vous avez terminé le module. |
**La difficulté monte.** Dans chaque module, les derniers exercices sont volontairement
plus exigeants, et le module 6 l'est encore davantage. C'est le principe d'un sujet de
bac : les dernières questions servent à voir jusqu'où vont les meilleurs. Ne pas les
terminer n'est pas un échec ; les faire, c'est du bonus.
**Auto-correction.** Chaque exercice de production est suivi d'une cellule de tests.
Exécutez-la : si `OK` s'affiche, c'est gagné. Si vous obtenez une `AssertionError`,
c'est que le code ne fait pas encore ce qui est demandé.
**Bloqué ?** Chaque module commence par une cellule **SECOURS** qui remet en place
toutes les données dont il a besoin. Les modules sont **indépendants** : vous pouvez
passer au suivant sans avoir fini le précédent.
---
*Objectif de la séance : redémarrer le moteur après deux mois d'arrêt. Rien de ce qui suit
n'est nouveau : tout a été vu en Première. Mais chaque notion révisée ici sert directement
un chapitre de Terminale.*
""")
# ---------------------------------------------------------------- MODULE 1
md("""---
# Module 1 : LE RÉACTEUR
### *Variables, affectation, mutabilité*
> Le réacteur tourne. C'est déjà ça. Mais les jauges du poste de pilotage affichent
> n'importe quoi : 4 % d'oxygène ici, 340 % là. Avant de réparer un seul programme,
> il faut comprendre **comment l'ordinateur de bord range ses valeurs en mémoire**.
*Ce module prépare : POO (les attributs d'un objet sont des variables), et les listes
chaînées (où deux variables peuvent pointer vers le même maillon).*
""")
md("### SECOURS : exécutez cette cellule pour démarrer le module 1")
code("""# Relevés des capteurs au réveil
TAUX_O2_NOMINAL = 21 # en pourcentage
PRESSION_NOMINALE = 101 # en kilopascals
""")
md("""## Diagnostic 1.1 : l'ordre des affectations
L'ordinateur a exécuté ces quatre lignes. **Avant d'exécuter la cellule**, écrivez ce que
vous pensez qu'elle va afficher.
*Rappel : une affectation `a = b` calcule d'abord la valeur de droite, puis la range dans
la variable de gauche. Ce qui est calculé est calculé, ça ne se recalcule pas tout seul.*
""")
code("""oxygene = 21
oxygene = oxygene - 4
pression = oxygene * 2
oxygene = 21
print(oxygene, pression)
# Ma prédiction :
""",
"""oxygene = 21
oxygene = oxygene - 4
pression = oxygene * 2
oxygene = 21
print(oxygene, pression)
# Ma prédiction : 21 34
# La ligne 3 a utilisé la valeur d'oxygene à cet instant (17), pas sa valeur finale.
# pression vaut donc 34 et ne « suit » pas les changements ultérieurs d'oxygene.
""")
md("""## Diagnostic 1.2 : deux noms, une seule liste
Celui-ci piège tout le monde. Prédisez, puis exécutez.
""")
code("""reserve_principale = [120, 80, 50]
reserve_secours = reserve_principale
reserve_secours.append(0)
print("principale :", reserve_principale)
print("secours :", reserve_secours)
# Ma prédiction :
""",
"""reserve_principale = [120, 80, 50]
reserve_secours = reserve_principale
reserve_secours.append(0)
print("principale :", reserve_principale)
print("secours :", reserve_secours)
# Ma prédiction : les deux affichent [120, 80, 50, 0]
# `reserve_secours = reserve_principale` ne copie PAS la liste : elle donne un
# deuxième nom à la même liste en mémoire. Modifier l'une modifie l'autre.
# Pour obtenir une vraie copie : reserve_secours = list(reserve_principale)
""")
md("""> **À retenir.** Une liste est **mutable** : on peut la modifier sur place.
> Un entier ou une chaîne ne le sont pas. C'est pour ça que le piège ci-dessus
> n'existe pas avec des entiers.
""")
md("""## Réparation 1.3 : l'échange raté
Le programme de bord doit **échanger** les valeurs de deux capteurs mal branchés.
Le technicien précédent a écrit ceci. Ça ne marche pas.
**Corrigez** la cellule pour que l'échange fonctionne réellement.
""")
code("""temp_interne = 18
temp_externe = -270
# --- échange (cassé) ---
temp_interne = temp_externe
temp_externe = temp_interne
# -----------------------
print(temp_interne, temp_externe)
""",
"""temp_interne = 18
temp_externe = -270
# --- échange (corrigé) ---
temp_interne, temp_externe = temp_externe, temp_interne
# Variante sans affectation multiple, avec une variable tampon :
# tampon = temp_interne
# temp_interne = temp_externe
# temp_externe = tampon
# -------------------------
print(temp_interne, temp_externe)
""")
code("""assert temp_interne == -270 and temp_externe == 18
print("OK. Échange réussi")
""")
md("""## Production 1.4 : calcul de l'autonomie
Il reste `litres_oxygene` litres d'oxygène dans les réserves. Chaque membre d'équipage
consomme `consommation_par_heure` litres par heure.
Calculez dans une variable **`autonomie_h`** le nombre **entier** d'heures complètes
que l'équipage peut encore tenir.
*Indice : `//` est la division entière, celle qui ne garde que le nombre de tours complets.*
""")
code("""litres_oxygene = 480
consommation_par_heure = 3
nb_membres = 4
autonomie_h = ... # à compléter
print("Autonomie :", autonomie_h, "heures")
""",
"""litres_oxygene = 480
consommation_par_heure = 3
nb_membres = 4
autonomie_h = litres_oxygene // (consommation_par_heure * nb_membres)
print("Autonomie :", autonomie_h, "heures")
""")
code("""assert autonomie_h == 40, "Attendu 40, obtenu " + str(autonomie_h)
print("OK. Autonomie correcte : 40 h")
""")
md("""## Bonus 1.5 : copie indépendante
Créez `sauvegarde` : une copie de `releves` qui **ne bouge pas** quand on modifie `releves`.
""")
code("""releves = [21, 101, 18]
sauvegarde = ... # à compléter
releves.append(999)
print("releves :", releves)
print("sauvegarde:", sauvegarde)
""",
"""releves = [21, 101, 18]
sauvegarde = list(releves) # ou releves[:] , ou releves.copy()
releves.append(999)
print("releves :", releves)
print("sauvegarde:", sauvegarde)
""")
code("""assert sauvegarde == [21, 101, 18]
assert releves == [21, 101, 18, 999]
print("OK. La sauvegarde est bien indépendante")
""")
# ---------------------------------------------------------------- MODULE 2
md("""---
# Module 2 : LES PROTOCOLES
### *Fonctions : paramètres, `return`, portée*
> Les protocoles de sécurité de la station sont des **fonctions**. On les appelle, elles
> répondent. Sauf que depuis les radiations, plusieurs d'entre elles ne répondent plus
> rien du tout : elles se contentent de parler dans le vide.
*Ce module prépare : la **récursivité** (une fonction qui s'appelle elle-même n'a de
sens que si elle **renvoie** quelque chose) et la Modularité.*
""")
md("### SECOURS : exécutez cette cellule pour démarrer le module 2")
code("""SEUIL_CRITIQUE = 80
SEUIL_ALERTE = 95
""")
md("""## Diagnostic 2.1 : `print` n'est pas `return`
Prédisez les **deux** lignes affichées.
""")
code("""def niveau_pression(pression):
if pression < 80:
print("CRITIQUE")
else:
print("NOMINAL")
resultat = niveau_pression(45)
print("resultat vaut :", resultat)
# Ma prédiction :
""",
"""def niveau_pression(pression):
if pression < 80:
print("CRITIQUE")
else:
print("NOMINAL")
resultat = niveau_pression(45)
print("resultat vaut :", resultat)
# Ma prédiction : CRITIQUE puis resultat vaut : None
# La fonction AFFICHE mais ne RENVOIE rien. Une fonction sans return renvoie None.
# On ne peut donc rien faire du résultat : ni le tester, ni le stocker, ni le réutiliser.
""")
md("""> **À retenir.** `print` **montre** à l'humain. `return` **rend** une valeur au
> programme. Une fonction qui ne fait que `print` est un cul-de-sac : on ne peut pas
> construire quoi que ce soit par-dessus.
""")
md("""## Réparation 2.2 : rendre le protocole utilisable
Réécrivez `niveau_pression` pour qu'elle **renvoie** la chaîne au lieu de l'afficher.
- pression strictement inférieure à 80 : renvoie `"CRITIQUE"`
- pression entre 80 (inclus) et 95 (exclu) : renvoie `"ALERTE"`
- pression supérieure ou égale à 95 : renvoie `"NOMINAL"`
""")
code("""def niveau_pression(pression):
... # à compléter
""",
"""def niveau_pression(pression):
if pression < 80:
return "CRITIQUE"
elif pression < 95:
return "ALERTE"
else:
return "NOMINAL"
""")
code("""assert niveau_pression(45) == "CRITIQUE"
assert niveau_pression(79) == "CRITIQUE"
assert niveau_pression(80) == "ALERTE"
assert niveau_pression(94) == "ALERTE"
assert niveau_pression(95) == "NOMINAL"
assert niveau_pression(120) == "NOMINAL"
print("OK. Protocole de pression opérationnel")
""")
md("""## Diagnostic 2.3 : la variable qui n'existe pas
Ce code **plante**. Avant de l'exécuter, essayez de dire **pourquoi**.
""")
code("""compteur_alertes = 0
def signaler_alerte():
compteur_alertes = compteur_alertes + 1
return compteur_alertes
print(signaler_alerte())
# Ma prédiction (quelle erreur, et pourquoi ?) :
""",
"""compteur_alertes = 0
def signaler_alerte():
compteur_alertes = compteur_alertes + 1
return compteur_alertes
print(signaler_alerte())
# Ma prédiction : UnboundLocalError.
# Comme `compteur_alertes` reçoit une affectation DANS la fonction, Python la considère
# comme une variable LOCALE. Au moment du calcul `compteur_alertes + 1`, cette variable
# locale n'a encore aucune valeur : erreur.
# La bonne solution n'est PAS `global` : c'est de passer la valeur en PARAMÈTRE
# et de renvoyer la nouvelle (voir cellule suivante).
""")
md("""**Corrigez** en transformant `signaler_alerte` en une fonction qui prend le compteur
actuel en paramètre et renvoie le compteur augmenté de 1.
""")
code("""def signaler_alerte(compteur):
... # à compléter
compteur_alertes = 0
compteur_alertes = signaler_alerte(compteur_alertes)
compteur_alertes = signaler_alerte(compteur_alertes)
print(compteur_alertes)
""",
"""def signaler_alerte(compteur):
return compteur + 1
compteur_alertes = 0
compteur_alertes = signaler_alerte(compteur_alertes)
compteur_alertes = signaler_alerte(compteur_alertes)
print(compteur_alertes)
""")
code("""assert signaler_alerte(0) == 1
assert signaler_alerte(41) == 42
assert compteur_alertes == 2
print("OK. Compteur d'alertes réparé")
""")
md("""## Production 2.4 : une fonction qui en appelle une autre
Écrivez `diagnostic(pression, temperature)` qui renvoie une chaîne de la forme :
```
"Pression NOMINAL / Temperature 18"
```
Votre fonction **doit appeler** `niveau_pression` : ne recopiez pas les tests de seuil.
*Indice : pour coller des morceaux de texte, `+` fonctionne, mais il faut convertir les
nombres avec `str(...)`.*
""")
code("""def diagnostic(pression, temperature):
... # à compléter
""",
"""def diagnostic(pression, temperature):
return "Pression " + niveau_pression(pression) + " / Temperature " + str(temperature)
""")
code("""assert diagnostic(101, 18) == "Pression NOMINAL / Temperature 18"
assert diagnostic(45, -3) == "Pression CRITIQUE / Temperature -3"
assert diagnostic(88, 0) == "Pression ALERTE / Temperature 0"
print("OK. Diagnostic complet opérationnel")
""")
md("""## Bonus 2.5 : paramètre par défaut
Écrivez `verrouiller(module, code_acces="0000")` qui renvoie :
- `"MODULE VERROUILLE"` si le code fourni est `"0000"`
- `"CODE REFUSE"` sinon
""")
code("""def verrouiller(module, code_acces="0000"):
... # à compléter
""",
"""def verrouiller(module, code_acces="0000"):
if code_acces == "0000":
return "MODULE VERROUILLE"
else:
return "CODE REFUSE"
""")
code("""assert verrouiller("Sas") == "MODULE VERROUILLE"
assert verrouiller("Sas", "0000") == "MODULE VERROUILLE"
assert verrouiller("Sas", "1234") == "CODE REFUSE"
print("OK. Verrouillage opérationnel")
""")
# ---------------------------------------------------------------- MODULE 3
md("""---
# Module 3 : LE BALAYAGE
### *Boucles : `for`, `while`, accumulateur*
> Il faut maintenant **balayer** les 5 000 relevés de capteurs accumulés pendant
> l'hibernation, pour repérer les anomalies. Personne ne fera ça à la main.
*Ce module prépare : le parcours des arbres et des graphes, où l'on visite un à un
tous les éléments d'une structure sans savoir à l'avance combien il y en a.*
""")
md("### SECOURS : exécutez cette cellule pour démarrer le module 3")
code("""mesures = [12, 45, 7, 88, 23, 91, 4]
""")
md("""## Diagnostic 3.1 : combien de tours ?
Prédisez ce qui s'affiche, **et combien de lignes**.
""")
code("""i = 0
while i < 5:
print(i)
i = i + 2
print("fin, i vaut", i)
# Ma prédiction :
""",
"""i = 0
while i < 5:
print(i)
i = i + 2
print("fin, i vaut", i)
# Ma prédiction : 0, 2, 4 (trois lignes), puis « fin, i vaut 6 ».
# Le test est réévalué AVANT chaque tour. Après le tour où i valait 4, i passe à 6 :
# 6 < 5 est faux, la boucle s'arrête. i vaut donc 6 à la sortie, pas 5.
""")
md("""## Diagnostic 3.2 : NE PAS EXÉCUTER
Cette cellule est en mode texte : **ne l'exécutez pas**, elle ne s'arrêterait jamais.
```python
carburant = 100
while carburant > 0:
print("Purge en cours...")
carburant = 100
```
**Question (répondez dans la cellule ci-dessous) :** pourquoi cette boucle ne se
termine-t-elle jamais, et quelle est la plus petite modification qui la ferait terminer ?
""")
code("""# Ma réponse :
""",
"""# Ma réponse :
# La condition `carburant > 0` reste vraie éternellement parce que le corps de la boucle
# remet carburant à 100 au lieu de le faire décroître. Rien ne rapproche le programme de
# la condition d'arrêt.
# Plus petite correction : remplacer `carburant = 100` par une décrémentation,
# par exemple `carburant = carburant - 10`.
#
# Règle : dans un while, une variable au moins doit évoluer VERS la condition d'arrêt.
""")
md("""## Réparation 3.3 : le capteur oublié
Ce code doit afficher **toutes** les mesures. Il en rate une. Trouvez laquelle, et corrigez.
""")
code("""for i in range(1, len(mesures)):
print("capteur", i, ":", mesures[i])
""",
"""for i in range(len(mesures)): # range(len(...)) commence à 0, pas à 1
print("capteur", i, ":", mesures[i])
# La version initiale commençait à l'indice 1 et sautait donc mesures[0] (la valeur 12).
# En Python le premier indice d'une liste est 0.
""")
md("""## Production 3.4 : le maximum, à la main
Écrivez `maximum(valeurs)` qui renvoie la plus grande valeur d'une liste **non vide**,
**sans utiliser `max()`**.
*Méthode de l'accumulateur : on retient un « meilleur pour l'instant », et on le met à
jour à chaque tour.*
""")
code("""def maximum(valeurs):
... # à compléter
""",
"""def maximum(valeurs):
plus_grand = valeurs[0] # on part du premier élément, pas de 0 !
for v in valeurs:
if v > plus_grand:
plus_grand = v
return plus_grand
""")
code("""assert maximum([12, 45, 7, 88, 23, 91, 4]) == 91
assert maximum([5]) == 5
assert maximum([3, 3, 3]) == 3
assert maximum([-10, -3, -50]) == -3, "attention aux valeurs négatives : ne partez pas de 0"
print("OK. Détection du maximum opérationnelle")
""")
md("""## Production 3.5 : le premier capteur défaillant
Écrivez `premier_defaillant(valeurs, seuil)` qui renvoie **l'indice** de la première
mesure strictement inférieure à `seuil`, et `-1` si aucune ne l'est.
*Indice : ici on a besoin de l'indice, donc de `range(len(valeurs))`. Et on peut sortir
de la fonction dès qu'on a trouvé.*
""")
code("""def premier_defaillant(valeurs, seuil):
... # à compléter
""",
"""def premier_defaillant(valeurs, seuil):
for i in range(len(valeurs)):
if valeurs[i] < seuil:
return i # return sort immédiatement de la fonction
return -1 # atteint seulement si la boucle s'est terminée
""")
code("""assert premier_defaillant([12, 45, 7, 88], 20) == 0
assert premier_defaillant([45, 88, 7, 4], 10) == 2
assert premier_defaillant([45, 88, 91], 10) == -1
assert premier_defaillant([], 10) == -1
print("OK. Détection de défaillance opérationnelle")
""")
md("""## Bonus 3.6 : la sentinelle
Le capteur d'altitude envoie ses relevés en continu et termine sa transmission par la
valeur `-1`. Écrivez `somme_jusqu_a_sentinelle(flux)` qui additionne les valeurs
**jusqu'au premier `-1` exclu**. Les valeurs après le `-1` sont du bruit : on les ignore.
*Indice : un `while` avec deux conditions est plus naturel qu'un `for` ici.*
""")
code("""def somme_jusqu_a_sentinelle(flux):
... # à compléter
""",
"""def somme_jusqu_a_sentinelle(flux):
total = 0
i = 0
while i < len(flux) and flux[i] != -1:
total = total + flux[i]
i = i + 1
return total
""")
code("""assert somme_jusqu_a_sentinelle([10, 20, 30, -1, 999, 999]) == 60
assert somme_jusqu_a_sentinelle([-1, 5, 5]) == 0
assert somme_jusqu_a_sentinelle([1, 2, 3]) == 6
assert somme_jusqu_a_sentinelle([]) == 0
print("OK. Lecture du flux opérationnelle")
""")
# ---------------------------------------------------------------- MODULE 4
md("""---
# Module 4 : LA SOUTE
### *Listes : indices, tranches, listes de listes*
> La soute est organisée en rayonnages. L'inventaire numérique a survécu, mais les
> programmes qui l'exploitent, non.
*Ce module prépare : les structures linéaires (pile, file, listes chaînées) et le
tri fusion du chapitre « diviser pour régner ».*
""")
md("### SECOURS : exécutez cette cellule pour démarrer le module 4")
code("""soute = ["eau", "vivres", "outils", "piles", "filtres"]
# Chaque sous-liste est un rayonnage : [rayon_0, rayon_1, rayon_2]
# Chaque ligne = un étage de la soute.
stock = [[12, 5, 0],
[3, 8, 7],
[0, 0, 4],
[9, 1, 2]]
""")
md("""## Diagnostic 4.1 : indices et tranches
Prédisez les quatre affichages.
""")
code("""print(soute[0])
print(soute[-1])
print(soute[1:3])
print(len(soute[1:3]))
# Ma prédiction :
""",
"""print(soute[0])
print(soute[-1])
print(soute[1:3])
print(len(soute[1:3]))
# Ma prédiction :
# eau
# filtres
# ['vivres', 'outils']
# 2
# Une tranche [a:b] va de l'indice a INCLUS à l'indice b EXCLU : elle contient b - a
# éléments. L'indice -1 désigne le dernier élément.
""")
md("""## Réparation 4.2 : le bug du technicien
Ce code doit retirer de `soute` tout ce qui commence par la lettre `"p"`.
Il en oublie. **Exécutez-le d'abord** pour constater le problème, puis corrigez-le.
*Indice : modifier une liste pendant qu'on la parcourt décale les indices sous les pieds
de la boucle. La solution habituelle est de **construire une nouvelle liste** plutôt que
de retirer dans l'ancienne.*
""")
code("""soute_test = ["piles", "pinces", "eau", "outils"]
for objet in soute_test:
if objet[0] == "p":
soute_test.remove(objet)
print(soute_test) # devrait afficher ['eau', 'outils']
""",
"""soute_test = ["piles", "pinces", "eau", "outils"]
# Version corrigée : on construit une nouvelle liste au lieu de modifier celle qu'on parcourt
soute_gardee = []
for objet in soute_test:
if objet[0] != "p":
soute_gardee.append(objet)
soute_test = soute_gardee
print(soute_test) # ['eau', 'outils']
# Pourquoi la version initiale échouait : après avoir retiré "piles" (indice 0), tous les
# éléments se décalent d'un cran. La boucle passe alors à l'indice 1, qui contient
# désormais "eau" : "pinces" a été SAUTÉ.
""")
code("""assert soute_test == ["eau", "outils"]
print("OK. Purge de la soute correcte")
""")
md("""## Production 4.3 : total d'un rayonnage
`stock` est une **liste de listes** : `stock[i][j]` est la quantité stockée à l'étage `i`,
rayon `j`.
Écrivez `total_rayon(grille, r)` qui renvoie la somme des quantités du rayon `r`,
**tous étages confondus**.
*Attention : le rayon `r` est une **colonne**, pas une ligne. Il faut parcourir les étages.*
""")
code("""def total_rayon(grille, r):
... # à compléter
""",
"""def total_rayon(grille, r):
total = 0
for etage in grille: # etage est une liste : [rayon_0, rayon_1, rayon_2]
total = total + etage[r]
return total
""")
code("""assert total_rayon(stock, 0) == 24
assert total_rayon(stock, 1) == 14
assert total_rayon(stock, 2) == 13
print("OK. Inventaire par rayon opérationnel")
""")
md("""## Production 4.4 : filtrer en une ligne
Écrivez `au_dessus(valeurs, seuil)` qui renvoie la liste des valeurs strictement
supérieures à `seuil`, **en utilisant une compréhension de liste**.
*Rappel de la forme : `[expression for element in liste if condition]`*
""")
code("""def au_dessus(valeurs, seuil):
... # à compléter (une seule ligne, avec une compréhension)
""",
"""def au_dessus(valeurs, seuil):
return [v for v in valeurs if v > seuil]
""")
code("""assert au_dessus([12, 45, 7, 88, 23], 20) == [45, 88, 23]
assert au_dessus([1, 2, 3], 10) == []
assert au_dessus([], 0) == []
print("OK. Filtrage opérationnel")
""")
md("""## Bonus 4.5 : fusionner deux listes triées
Deux capteurs ont enregistré des relevés, chacun **déjà trié dans l'ordre croissant**.
Écrivez `fusion(a, b)` qui renvoie une seule liste triée contenant tous les éléments,
**sans utiliser `sorted()` ni `.sort()`**.
*Méthode : deux indices qui avancent, on prend à chaque tour le plus petit des deux
éléments courants.*
> Cet exercice est **exactement** la seconde moitié du tri fusion, que vous verrez
> dans le chapitre « diviser pour régner ».
""")
code("""def fusion(a, b):
... # à compléter
""",
"""def fusion(a, b):
resultat = []
i = 0
j = 0
while i < len(a) and j < len(b):
if a[i] <= b[j]:
resultat.append(a[i])
i = i + 1
else:
resultat.append(b[j])
j = j + 1
# une des deux listes est épuisée : on recopie ce qui reste de l'autre
while i < len(a):
resultat.append(a[i])
i = i + 1
while j < len(b):
resultat.append(b[j])
j = j + 1
return resultat
""")
code("""assert fusion([1, 4, 9], [2, 3, 10]) == [1, 2, 3, 4, 9, 10]
assert fusion([], [2, 3]) == [2, 3]
assert fusion([5], []) == [5]
assert fusion([1, 1], [1]) == [1, 1, 1]
print("OK. Fusion opérationnelle, vous venez d'écrire la moitié d'un tri fusion")
""")
# ---------------------------------------------------------------- MODULE 5
md("""---
# Module 5 : L'ÉQUIPAGE
### *Dictionnaires : clés, valeurs, parcours*
> Reste le plus important : les **dossiers de l'équipage** et le **plan de la station**.
> Une liste ne suffit plus : on ne cherche pas « le 3ᵉ membre », on cherche « Kowalski ».
*Ce module prépare : les graphes (un graphe se représente par un dictionnaire de
listes) et la mémoïsation en programmation dynamique (un dictionnaire de résultats
déjà calculés).*
""")
md("### SECOURS : exécutez cette cellule pour démarrer le module 5")
code("""equipage = {
"Kowalski": "pilote",
"Ndiaye": "medecin",
"Reyes": "ingenieur",
"Sokolova": "pilote",
"Tanaka": "ingenieur",
}
# Plan de la station : à chaque module, la liste des modules directement accessibles
plan = {
"Sas": ["Couloir"],
"Couloir": ["Sas", "Cockpit", "Soute", "Labo"],
"Cockpit": ["Couloir"],
"Soute": ["Couloir", "Reacteur"],
"Labo": ["Couloir"],
"Reacteur": ["Soute"],
}
""")
md("""## Diagnostic 5.1 : accès et absence
Prédisez le résultat des trois premières lignes. La quatrième est volontairement
commentée : dites ce qui se passerait si on la décommentait.
""")
code("""print(equipage["Reyes"])
print(len(equipage))
print("Bowman" in equipage)
# print(equipage["Bowman"])
# Ma prédiction :
""",
"""print(equipage["Reyes"])
print(len(equipage))
print("Bowman" in equipage)
# print(equipage["Bowman"])
# Ma prédiction :
# ingenieur
# 5 (len d'un dictionnaire = son nombre de clés)
# False
# La 4e ligne lèverait une KeyError : accéder à une clé absente est une ERREUR,
# ce n'est pas None. Pour éviter ça : equipage.get("Bowman", "inconnu").
""")
md("""## Réparation 5.2 : le poste qui n'existe pas
Cette fonction plante dès qu'on lui donne un nom inconnu. Corrigez-la pour qu'elle
renvoie `"inconnu"` dans ce cas.
""")
code("""def poste_de(registre, nom):
return registre[nom]
print(poste_de(equipage, "Tanaka"))
print(poste_de(equipage, "Bowman")) # plante
""",
"""def poste_de(registre, nom):
return registre.get(nom, "inconnu")
# Variante explicite, tout aussi correcte :
# def poste_de(registre, nom):
# if nom in registre:
# return registre[nom]
# return "inconnu"
print(poste_de(equipage, "Tanaka"))
print(poste_de(equipage, "Bowman"))
""")
code("""assert poste_de(equipage, "Tanaka") == "ingenieur"
assert poste_de(equipage, "Bowman") == "inconnu"
print("OK. Registre sécurisé")
""")
md("""## Production 5.3 : compter les postes
Écrivez `compter_postes(registre)` qui renvoie un **dictionnaire** associant à chaque
poste le nombre de membres qui l'occupent.
Sur `equipage`, on doit obtenir : `{'pilote': 2, 'medecin': 1, 'ingenieur': 2}`
*Méthode classique : on part d'un dictionnaire vide, et pour chaque valeur rencontrée,
soit la clé existe déjà et on l'incrémente, soit elle n'existe pas et on la crée à 1.*
""")
code("""def compter_postes(registre):
... # à compléter
""",
"""def compter_postes(registre):
comptes = {}
for poste in registre.values():
if poste in comptes:
comptes[poste] = comptes[poste] + 1
else:
comptes[poste] = 1
return comptes
""")
code("""resultat = compter_postes(equipage)
assert resultat == {"pilote": 2, "medecin": 1, "ingenieur": 2}
assert compter_postes({}) == {}
assert compter_postes({"A": "pilote"}) == {"pilote": 1}
print("OK. Recensement de l'équipage opérationnel")
""")
md("""## Production 5.4 : lire le plan de la station
`plan` est un dictionnaire dont **les valeurs sont des listes**.
1. Écrivez `voisins(plan, module)` : la liste des modules directement accessibles depuis
`module`, ou `[]` si le module n'existe pas.
2. Écrivez `nb_liaisons(plan)` : le nombre **total** de liaisons listées dans le plan
(on compte simplement la longueur de toutes les listes).
> Cette structure a un nom : c'est une **liste d'adjacence**. C'est celle que vous
> utiliserez pour représenter un graphe.
""")
code("""def voisins(plan, module):
... # à compléter
def nb_liaisons(plan):
... # à compléter
""",
"""def voisins(plan, module):
return plan.get(module, [])
def nb_liaisons(plan):
total = 0
for liste_voisins in plan.values():
total = total + len(liste_voisins)
return total
""")
code("""assert voisins(plan, "Cockpit") == ["Couloir"]
assert voisins(plan, "Couloir") == ["Sas", "Cockpit", "Soute", "Labo"]
assert voisins(plan, "Hangar") == []
assert nb_liaisons(plan) == 10
print("OK. Lecture du plan opérationnelle")
""")
md("""## Bonus 5.5 : regrouper par poste
Écrivez `par_poste(registre)` qui renvoie un dictionnaire associant à chaque poste la
**liste des noms** qui l'occupent :
```python
{'pilote': ['Kowalski', 'Sokolova'], 'medecin': ['Ndiaye'], 'ingenieur': ['Reyes', 'Tanaka']}
```
""")
code("""def par_poste(registre):
... # à compléter
""",
"""def par_poste(registre):
groupes = {}
for nom, poste in registre.items():
if poste not in groupes:
groupes[poste] = []
groupes[poste].append(nom)
return groupes
""")
code("""attendu = {"pilote": ["Kowalski", "Sokolova"],
"medecin": ["Ndiaye"],
"ingenieur": ["Reyes", "Tanaka"]}
assert par_poste(equipage) == attendu
assert par_poste({}) == {}
print("OK. Regroupement opérationnel")
""")
# ---------------------------------------------------------------- MODULE 6
md("""---
# Module 6 : SORTIE D'ORBITE
### *Synthèse, et retour à la récursivité*
> Tous les systèmes répondent. Reste à vérifier qu'aucun module n'est resté isolé après
> les avaries : si un compartiment n'est plus relié au reste de la station, l'équipage
> qui s'y trouve est piégé.
""")
md("### SECOURS : exécutez cette cellule pour démarrer le module 6")
code("""plan = {
"Sas": ["Couloir"],
"Couloir": ["Sas", "Cockpit", "Soute", "Labo"],
"Cockpit": ["Couloir"],
"Soute": ["Couloir", "Reacteur"],
"Labo": ["Couloir"],
"Reacteur": ["Soute"],
"Serre": ["Cellule"], # compartiment isolé du reste !
"Cellule": ["Serre"],
}
""")
md("""## Synthèse 6.1 : quels modules sont atteignables ?
Écrivez `atteignables(plan, depart)` : la liste des modules que l'on peut rejoindre
depuis `depart`, en comptant `depart` lui-même.
L'algorithme vous est donné, il ne reste qu'à le traduire :
```
a_visiter ← liste contenant seulement depart
visites ← liste vide
tant que a_visiter n'est pas vide :
module ← on retire le DERNIER élément de a_visiter
si module n'est pas déjà dans visites :
ajouter module à visites
pour chaque v dans les voisins de module :
ajouter v à a_visiter
renvoyer visites
```
*Indice : « retirer le dernier élément » se dit `liste.pop()` en Python.*
> Ce que vous venez d'écrire est un **parcours en profondeur**. `a_visiter` est une
> **pile**. Vous retrouverez les deux, sous ces noms, dans les chapitres Pile/File et
> Graphes.
""")
code("""def atteignables(plan, depart):
... # à compléter
""",
"""def atteignables(plan, depart):
a_visiter = [depart]
visites = []
while len(a_visiter) > 0:
module = a_visiter.pop() # retire et renvoie le dernier élément
if module not in visites:
visites.append(module)
for v in plan.get(module, []):
a_visiter.append(v)
return visites
""")
code("""r = atteignables(plan, "Sas")
assert sorted(r) == ["Cockpit", "Couloir", "Labo", "Reacteur", "Sas", "Soute"]
assert "Serre" not in r, "La serre ne doit PAS être atteignable depuis le sas"
assert sorted(atteignables(plan, "Serre")) == ["Cellule", "Serre"]
assert atteignables(plan, "Hangar") == ["Hangar"]
print("OK. Cartographie terminée, 2 compartiments isolés détectés : Serre et Cellule")
""")
md("""## Diagnostic 6.2 : le compte à rebours
Dernière étape avant le décollage. **Ne l'exécutez pas tout de suite** : écrivez d'abord,
sur votre feuille, l'ordre exact des affichages pour `compte_a_rebours(3)`.
""")
code("""def compte_a_rebours(n):
if n == 0:
print("DECOLLAGE")
else:
print(n)
compte_a_rebours(n - 1)
print("verif", n)
compte_a_rebours(3)
# Ma prédiction :
""",
"""def compte_a_rebours(n):
if n == 0:
print("DECOLLAGE")
else:
print(n)
compte_a_rebours(n - 1)
print("verif", n)
compte_a_rebours(3)
# Ma prédiction :
# 3
# 2
# 1
# DECOLLAGE
# verif 1
# verif 2
# verif 3
#
# Les « verif » sortent dans l'ORDRE INVERSE : chaque appel est mis en attente pendant
# que l'appel suivant se déroule, puis reprend là où il s'était arrêté. C'est exactement
# la PILE D'APPELS du chapitre en cours.
""")
md("""---
## Fin du protocole
**PERRIAND-7 est de nouveau opérationnelle.**
Ce que vous venez de réutiliser, et où ça resservira :
| Module | Notion | Chapitre de Terminale concerné |
|---|---|---|
| 1 · Réacteur | variables, mutabilité, aliasing | POO, listes chaînées |
| 2 · Protocoles | `return`, paramètres, portée | **Récursivité**, Modularité |
| 3 · Balayage | boucles, accumulateur | parcours d'arbres et de graphes |
| 4 · Soute | listes, listes de listes, compréhensions | structures linéaires, tri fusion |
| 5 · Équipage | dictionnaires, dict de listes | **graphes**, mémoïsation |
| 6 · Sortie d'orbite | pile, parcours, pile d'appels | Pile/File, **Récursivité** |
---
*Lycée Charlotte Perriand, NSI Terminale, Licence CC BY-SA 4.0*
""")
# ---------------------------------------------------------------- WRITE
def build(index):
cells = []
for kind, eleve, corrige in CELLS:
src = (eleve, corrige)[index]
lines = src.splitlines(keepends=True)
cells.append({
"cell_type": kind,
"id": uuid.uuid4().hex[:8],
"metadata": {},
"source": lines,
**({"execution_count": None, "outputs": []} if kind == "code" else {}),
})
return {
"cells": cells,
"metadata": {
"kernelspec": {"display_name": "Python 3", "language": "python", "name": "python3"},
"language_info": {"name": "python", "version": "3.11"},
},
"nbformat": 4,
"nbformat_minor": 5,
}
DEST = os.path.expanduser("~/Documents/Cours/NSI/Terminale/Revisions_Rentree")
os.makedirs(DEST, exist_ok=True)
for idx, name in ((0, "PERRIAND-7_Reveil_des_systemes.ipynb"),
(1, "PERRIAND-7_CORRIGE.ipynb")):
with open(os.path.join(DEST, name), "w", encoding="utf-8") as f:
json.dump(build(idx), f, ensure_ascii=False, indent=1)
print("écrit :", name, len(CELLS), "cellules")