# -*- 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")