Files
TermNSI/Revisions_Rentree/PERRIAND-7_CORRIGE.ipynb
T

46 KiB

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 : 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).

SECOURS : exécutez cette cellule pour démarrer le module 1

In [ ]:
# Relevés des capteurs au réveil
TAUX_O2_NOMINAL = 21        # en pourcentage
PRESSION_NOMINALE = 101     # en kilopascals

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.

In [ ]:
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.

Diagnostic 1.2 : deux noms, une seule liste

Celui-ci piège tout le monde. Prédisez, puis exécutez.

In [ ]:
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)

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

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.

In [ ]:
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)
In [ ]:
assert temp_interne == -270 and temp_externe == 18
print("OK. Échange réussi")

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.

In [ ]:
litres_oxygene = 480
consommation_par_heure = 3
nb_membres = 4

autonomie_h = litres_oxygene // (consommation_par_heure * nb_membres)

print("Autonomie :", autonomie_h, "heures")
In [ ]:
assert autonomie_h == 40, "Attendu 40, obtenu " + str(autonomie_h)
print("OK. Autonomie correcte : 40 h")

Bonus 1.5 : copie indépendante

Créez sauvegarde : une copie de releves qui ne bouge pas quand on modifie releves.

In [ ]:
releves = [21, 101, 18]

sauvegarde = list(releves)     # ou releves[:] , ou releves.copy()

releves.append(999)
print("releves   :", releves)
print("sauvegarde:", sauvegarde)
In [ ]:
assert sauvegarde == [21, 101, 18]
assert releves == [21, 101, 18, 999]
print("OK. La sauvegarde est bien indépendante")

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

SECOURS : exécutez cette cellule pour démarrer le module 2

In [ ]:
SEUIL_CRITIQUE = 80
SEUIL_ALERTE = 95

Diagnostic 2.1 : print n'est pas return

Prédisez les deux lignes affichées.

In [ ]:
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.

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

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"
In [ ]:
def niveau_pression(pression):
    if pression < 80:
        return "CRITIQUE"
    elif pression < 95:
        return "ALERTE"
    else:
        return "NOMINAL"
In [ ]:
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")

Diagnostic 2.3 : la variable qui n'existe pas

Ce code plante. Avant de l'exécuter, essayez de dire pourquoi.

In [ ]:
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).

Corrigez en transformant signaler_alerte en une fonction qui prend le compteur actuel en paramètre et renvoie le compteur augmenté de 1.

In [ ]:
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)
In [ ]:
assert signaler_alerte(0) == 1
assert signaler_alerte(41) == 42
assert compteur_alertes == 2
print("OK. Compteur d'alertes réparé")

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(...).

In [ ]:
def diagnostic(pression, temperature):
    return "Pression " + niveau_pression(pression) + " / Temperature " + str(temperature)
In [ ]:
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")

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
In [ ]:
def verrouiller(module, code_acces="0000"):
    if code_acces == "0000":
        return "MODULE VERROUILLE"
    else:
        return "CODE REFUSE"
In [ ]:
assert verrouiller("Sas") == "MODULE VERROUILLE"
assert verrouiller("Sas", "0000") == "MODULE VERROUILLE"
assert verrouiller("Sas", "1234") == "CODE REFUSE"
print("OK. Verrouillage opérationnel")

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.

SECOURS : exécutez cette cellule pour démarrer le module 3

In [ ]:
mesures = [12, 45, 7, 88, 23, 91, 4]

Diagnostic 3.1 : combien de tours ?

Prédisez ce qui s'affiche, et combien de lignes.

In [ ]:
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.

Diagnostic 3.2 : NE PAS EXÉCUTER

Cette cellule est en mode texte : ne l'exécutez pas, elle ne s'arrêterait jamais.

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 ?

In [ ]:
# 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.

Réparation 3.3 : le capteur oublié

Ce code doit afficher toutes les mesures. Il en rate une. Trouvez laquelle, et corrigez.

In [ ]:
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.

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.

In [ ]:
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
In [ ]:
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")

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

In [ ]:
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
In [ ]:
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")

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.

In [ ]:
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
In [ ]:
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 : 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 ».

SECOURS : exécutez cette cellule pour démarrer le module 4

In [ ]:
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]]

Diagnostic 4.1 : indices et tranches

Prédisez les quatre affichages.

In [ ]:
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.

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.

In [ ]:
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É.
In [ ]:
assert soute_test == ["eau", "outils"]
print("OK. Purge de la soute correcte")

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.

In [ ]:
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
In [ ]:
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")

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]

In [ ]:
def au_dessus(valeurs, seuil):
    return [v for v in valeurs if v > seuil]
In [ ]:
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")

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

In [ ]:
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
In [ ]:
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 : 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).

SECOURS : exécutez cette cellule pour démarrer le module 5

In [ ]:
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"],
}

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.

In [ ]:
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").

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.

In [ ]:
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"))
In [ ]:
assert poste_de(equipage, "Tanaka") == "ingenieur"
assert poste_de(equipage, "Bowman") == "inconnu"
print("OK. Registre sécurisé")

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.

In [ ]:
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
In [ ]:
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")

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.

In [ ]:
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
In [ ]:
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")

Bonus 5.5 : regrouper par poste

Écrivez par_poste(registre) qui renvoie un dictionnaire associant à chaque poste la liste des noms qui l'occupent :

{'pilote': ['Kowalski', 'Sokolova'], 'medecin': ['Ndiaye'], 'ingenieur': ['Reyes', 'Tanaka']}
In [ ]:
def par_poste(registre):
    groupes = {}
    for nom, poste in registre.items():
        if poste not in groupes:
            groupes[poste] = []
        groupes[poste].append(nom)
    return groupes
In [ ]:
attendu = {"pilote": ["Kowalski", "Sokolova"],
           "medecin": ["Ndiaye"],
           "ingenieur": ["Reyes", "Tanaka"]}
assert par_poste(equipage) == attendu
assert par_poste({}) == {}
print("OK. Regroupement opérationnel")

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

SECOURS : exécutez cette cellule pour démarrer le module 6

In [ ]:
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"],
}

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.

In [ ]:
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
In [ ]:
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")

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

In [ ]:
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.

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