Me contacter

NSI Première • Programme français, lycées de Montréal

Exercices corrigés de NSI Première : les bases de Python

Voici une série d'exercices corrigés de NSI de Première, sur les bases de la programmation Python. Elle s'adresse aux élèves des lycées français de Montréal, le Lycée Marie de France et le Collège Stanislas, qui suivent le programme français de la spécialité Numérique et Sciences Informatiques.

Un conseil de méthode avant de commencer : ne lisez pas le code, exécutez-le à la main. Prenez une feuille, faites un tableau avec une colonne par variable, et remplissez une ligne par tour de boucle. C'est exactement ce que l'épreuve attend quand elle demande « que renvoie cette fonction », et c'est la seule façon de trouver un bug sans machine. La partie B est entièrement construite là-dessus.

Rappel de cours

  • Division : a // ba\ //\ b est le quotient entier (arrondi vers le bas, y compris pour les négatifs) et a % ba\ \%\ b le reste ; a / ba\ /\ b renvoie toujours un flottant.
  • Flottants : ne jamais tester l'égalité de deux flottants avec ====. On compare avec une tolérance : abs(a - b) < 1e-9.
  • Booléens : lois de De Morgan, non(a et b) = (non a) ou (non b). Python évalue « et » et « ou » en court-circuit, de gauche à droite.
  • Boucle for i in range(a, b, p) : i part de a, avance de p, et s'arrête AVANT b. range(n) donne 0, 1, ..., n-1.
  • Terminaison d'une boucle while : exhiber un variant, une quantité entière positive qui décroît strictement à chaque tour.
  • Tranches : t[i:j] contient les indices i à j-1 ; t[::-1] renvoie la séquence renversée ; les tranches créent une NOUVELLE liste.
  • Tableau à deux dimensions : t[i][j] désigne la ligne i, colonne j. len(t) est le nombre de lignes, len(t[0]) le nombre de colonnes.

Partie A : Les bases (/50)

Exercice 1 : Types, opérateurs et résultats surprenants

Toutes les questions demandent la valeur ET le type du résultat. Répondez sans machine : l'épreuve se passe sans interpréteur, et savoir prévoir ce que Python va faire est précisément la compétence évaluée.

  • a) Donnez la valeur et le type de : 7 // 2, 7 / 2, 7 % 2, 2 ** 10.
  • b) Donnez la valeur de -7 // 2 et de -7 % 2. Le résultat de la division entière vous surprend-il ? Expliquez la règle utilisée par Python.
  • c) Que vaut 0.1 + 0.2 ? L'expression 0.1 + 0.2 == 0.3 est-elle vraie ? Justifiez, puis proposez la bonne façon de comparer deux flottants.
  • d) Donnez la valeur de : '3' + '4', '3' * 4, int(3.9), int(-3.9).
  • e) Que valent round(2.5), round(3.5) et round(0.5) ? Formulez la règle d'arrondi réellement appliquée par Python.
Voir la correction

a) 7 // 2 vaut 3 (int) : c'est le quotient entier. 7 / 2 vaut 3.5 (float) : l'opérateur / renvoie TOUJOURS un flottant, même quand la division tombe juste. 7 % 2 vaut 1 (int), le reste. 2 ** 10 vaut 1024 (int).

b) -7 // 2 vaut -4, et non -3. Python arrondit le quotient vers le bas (vers moins l'infini), pas vers zéro. Et -7 % 2 vaut 1, un reste positif. Les deux sont cohérents : la relation a=b×(a // b)+(a % b)a = b \times (a\ //\ b) + (a\ \%\ b) doit rester vraie, et en effet 7=2×(4)+1-7 = 2\times(-4)+1. En Python, le reste a toujours le signe du diviseur.

c) 0.1 + 0.2 vaut 0.30000000000000004, donc l'expression est FAUSSE. En binaire, 0,1 et 0,2 n'ont pas d'écriture finie, exactement comme 1/3 n'en a pas en base 10 : la machine en stocke une approximation, et les erreurs s'additionnent. On ne teste donc jamais l'égalité de deux flottants ; on écrit abs(a - b) < 1e-9, ou l'on travaille avec des entiers quand c'est possible (des centimes plutôt que des euros).

d) '3' + '4' vaut '34' : entre deux chaînes, + est la concaténation. '3' * 4 vaut '3333' : chaîne fois entier, c'est la répétition. int(3.9) vaut 3 et int(-3.9) vaut -3 : int() tronque vers zéro, elle n'arrondit pas (attention, ce n'est donc PAS la même chose que //).

e) round(2.5) vaut 2, round(3.5) vaut 4 et round(0.5) vaut 0. La règle n'est pas « 0,5 s'arrondit au-dessus » mais l'arrondi au pair le plus proche : en cas d'égalité parfaite, Python choisit l'entier pair. Ce choix évite le biais systématique vers le haut quand on arrondit de longues séries de valeurs.

Exercice 2 : Expressions booléennes, De Morgan et court-circuit

Une condition mal écrite est la première cause de bug en NSI. Ces questions portent sur l'équivalence logique et sur l'ordre d'évaluation.

Python
def acces(age, abonne, invite):
    if age >= 18:
        if abonne == True:
            return True
        else:
            if invite == True:
                return True
            else:
                return False
    else:
        return False
  • a) Dressez la table de vérité de non(a et b) et de (non a) ou (non b) pour les quatre couples possibles. Que constatez-vous ?
  • b) Réécrivez la fonction acces ci-dessus en UNE seule instruction return, sans aucun if.
  • c) On veut tester qu'un nombre x est non nul et que 10/x dépasse 2. Entre x != 0 and 10 / x > 2 et 10 / x > 2 and x != 0, une seule des deux écritures est correcte. Laquelle, et pourquoi l'autre plante-t-elle ?
  • d) Traduisez en Python : « x est compris entre 0 et 10 inclus ». Donnez la version courte propre à Python.
  • e) Simplifiez : if n % 2 == 0: return True else: return False.
Voir la correction

a) Pour (a, b) valant (F,F), (F,V), (V,F), (V,V), les deux expressions valent respectivement V, V, V, F. Elles coïncident dans les quatre cas : c'est la loi de De Morgan, non(a et b) = (non a) ou (non b). L'autre loi, symétrique, est non(a ou b) = (non a) et (non b).

b) Le corps entier revient à demander : l'utilisateur est majeur ET (abonné OU invité). D'où la version en une ligne : return age >= 18 and (abonne or invite). Deux remarques de style : les parenthèses sont indispensables car « and » est prioritaire sur « or », et on n'écrit jamais abonne == True mais simplement abonne, qui est déjà un booléen.

c) Seule x != 0 and 10 / x > 2 est correcte. Python évalue « and » en court-circuit et de gauche à droite : si x vaut 0, le premier facteur est faux, la deuxième condition n'est jamais évaluée et la division par zéro n'a pas lieu. Dans l'autre ordre, 10 / x est calculée en premier et lève une ZeroDivisionError pour x = 0. Le court-circuit n'est donc pas une optimisation, c'est un outil de protection.

d) On peut écrire x >= 0 and x <= 10, mais Python autorise les comparaisons chaînées : 0 <= x <= 10, qui se lit comme en mathématiques et n'évalue x qu'une fois.

e) return n % 2 == 0. La comparaison produit déjà un booléen : le if ne fait que le recopier. Écrire if cond: return True else: return False est toujours un signe qu'on peut renvoyer directement la condition.

Exercice 3 : Boucles, trace d'exécution et terminaison

La suite de Syracuse part d'un entier n strictement positif et applique la règle suivante : si n est pair on le divise par 2, sinon on le remplace par 3n + 1, jusqu'à atteindre 1.

Python
def syracuse(n):
    etapes = 0
    while n != 1:
        if n % 2 == 0:
            n = n // 2
        else:
            n = 3 * n + 1
        etapes += 1
    return etapes
  • a) Faites la trace complète de syracuse(6) : donnez la suite des valeurs prises par n, puis la valeur renvoyée.
  • b) Même travail pour n = 7. Donnez le nombre d'étapes et la plus grande valeur atteinte par n en cours de route.
  • c) Combien de tours effectue la boucle for i in range(3, 20, 4) ? Donnez les valeurs prises par i.
  • d) Pour prouver qu'une boucle while se termine, on exhibe un variant. Expliquez pourquoi la terminaison de syracuse est un cas très particulier, et ce qu'on sait réellement à son sujet.
  • e) Écrivez une fonction altitude(n) qui renvoie la plus grande valeur atteinte par la suite partant de n.
Voir la correction

a) La suite est 6, 3, 10, 5, 16, 8, 4, 2, 1. Chaque flèche est un tour de boucle, donc etapes vaut 8. Détail : 6 est pair donc 3 ; 3 est impair donc 10 ; 10 pair donc 5 ; 5 impair donc 16 ; puis 8, 4, 2, 1 par divisions successives.

b) Pour n = 7 : 7, 22, 11, 34, 17, 52, 26, 13, 40, 20, 10, 5, 16, 8, 4, 2, 1. Cela fait 16 étapes, et la plus grande valeur atteinte est 52. Noter que la suite monte largement au-dessus de son point de départ avant de retomber : c'est ce qui rend la terminaison non évidente.

c) i prend les valeurs 3, 7, 11, 15, 19, puis 23 dépasserait 20 et la boucle s'arrête : 5 tours. La borne de range est toujours exclue.

d) Un variant est une quantité entière positive qui décroît strictement à chaque tour, ce qui garantit l'arrêt. Ici, aucun variant n'est connu : n n'a aucune raison de décroître, il triple presque quand il est impair. La conjecture de Syracuse affirme que la suite atteint 1 pour tout entier de départ, mais elle n'est PAS démontrée, malgré une vérification par ordinateur jusqu'à des valeurs colossales. C'est donc un programme dont personne ne sait prouver qu'il termine toujours, tout en étant certain qu'il termine pour tous les cas testés.

e) Il suffit de mémoriser le maximum au fil du parcours.

Python
def altitude(n):
    maxi = n
    while n != 1:
        if n % 2 == 0:
            n = n // 2
        else:
            n = 3 * n + 1
        if n > maxi:
            maxi = n
    return maxi

# altitude(7) vaut 52

Exercice 4 : Fonctions, portée et le piège de l'argument par défaut

Le code ci-dessous compile et s'exécute sans erreur, mais son comportement surprend presque tout le monde la première fois.

Python
def ajouter(x, liste=[]):
    liste.append(x)
    return liste

print(ajouter(1))
print(ajouter(2))
print(ajouter(3))
  • a) Qu'affiche ce programme ? Donnez les trois lignes exactement.
  • b) Expliquez le mécanisme responsable de ce comportement.
  • c) Corrigez la fonction pour que chaque appel sans deuxième argument reparte d'une liste vide.
  • d) Quelle est la différence entre une fonction qui RENVOIE une valeur et une fonction qui MODIFIE son argument ? Illustrez avec deux versions d'une fonction qui double tous les éléments d'une liste.
  • e) Que vaut la variable n après l'exécution du code suivant ? n = 5 ; def f(): n = 10 ; f() ; print(n).
Voir la correction

a) L'affichage est [1], puis [1, 2], puis [1, 2, 3].

b) La valeur par défaut est évaluée UNE SEULE FOIS, au moment où Python lit la définition de la fonction, pas à chaque appel. Il existe donc une unique liste, créée une fois pour toutes et attachée à la fonction ; chaque appel sans deuxième argument travaille sur cette même liste, qui garde les ajouts précédents. La règle à retenir : on ne met jamais d'objet mutable (liste, dictionnaire) comme valeur par défaut.

c) On utilise None comme sentinelle et on crée la liste à l'intérieur du corps, donc à chaque appel.

d) Une fonction qui renvoie une valeur laisse son argument intact et produit un nouvel objet : on l'utilise avec t2 = double(t). Une fonction qui modifie son argument agit par effet de bord, ne renvoie rien (None), et s'utilise avec double_sur_place(t) sur une ligne seule. Le bug classique consiste à écrire t = double_sur_place(t), ce qui écrase t avec None. Il faut choisir l'un des deux styles et le documenter, jamais les deux à la fois.

e) n vaut 5. L'affectation n = 10 à l'intérieur de f crée une variable LOCALE qui porte le même nom et disparaît à la fin de l'appel ; elle ne touche pas la variable globale. Pour modifier la globale il faudrait déclarer global n, ce qu'on évite en pratique.

Python
def ajouter(x, liste=None):
    if liste is None:
        liste = []
    liste.append(x)
    return liste

def double(t):            # renvoie une nouvelle liste
    return [2 * x for x in t]

def double_sur_place(t):  # modifie t, ne renvoie rien
    for i in range(len(t)):
        t[i] = 2 * t[i]

Exercice 5 : Listes, tranches et chaînes de caractères

On pose t = [0, 1, 2, 3, 4, 5] et m = 'informatique'. Répondez sans machine, en donnant à chaque fois le résultat exact.

  • a) Donnez la valeur de t[1:4], t[::2], t[::-1] et t[-2:].
  • b) Que valent t[:0] et t[2:2] ? Que se passe-t-il si l'on écrit t[10] ? Et t[2:10] ?
  • c) Donnez la valeur de m[0], m[-1], m[0:5], m[::-1] et len(m).
  • d) Écrivez une expression qui construit la liste des carrés des entiers de 1 à 10, d'abord avec une boucle, puis en compréhension.
  • e) Quelle est la différence entre t2 = t et t2 = t[:] ? Illustrez par un exemple où le choix change le résultat.
Voir la correction

a) t[1:4] vaut [1, 2, 3] : les indices 1, 2 et 3, la borne de droite étant exclue. t[::2] vaut [0, 2, 4] : un élément sur deux. t[::-1] vaut [5, 4, 3, 2, 1, 0] : la liste renversée. t[-2:] vaut [4, 5] : les deux derniers, l'indice -1 désignant le dernier élément.

b) t[:0] et t[2:2] valent tous deux [] : une tranche vide n'est pas une erreur. En revanche t[10] lève une IndexError, car l'indexation d'un élément hors bornes est interdite. Mais t[2:10] vaut [2, 3, 4, 5] sans erreur : les TRANCHES sont tolérantes et se contentent de s'arrêter à la fin de la liste. C'est une asymétrie que l'épreuve aime tester.

c) m[0] vaut 'i', m[-1] vaut 'e', m[0:5] vaut 'infor', m[::-1] vaut 'euqitamrofni' et len(m) vaut 12. Une chaîne s'indexe et se tranche exactement comme une liste, à une différence près : elle est immuable, donc m[0] = 'I' est interdit.

d) Par boucle : créer une liste vide puis ajouter les carrés un par un. En compréhension, tout tient sur une ligne. Les deux produisent [1, 4, 9, 16, 25, 36, 49, 64, 81, 100].

e) t2 = t ne copie rien : les deux noms désignent le MÊME objet, donc modifier t2[0] change aussi t[0]. t2 = t[:] crée une vraie copie (superficielle) : les deux listes sont indépendantes. Exemple : après t = [1, 2, 3] et t2 = t, l'instruction t2[0] = 99 donne t = [99, 2, 3]. Avec t2 = t[:], t reste [1, 2, 3].

Python
carres = []
for i in range(1, 11):
    carres.append(i * i)

carres = [i * i for i in range(1, 11)]
# [1, 4, 9, 16, 25, 36, 49, 64, 81, 100]

Partie B : Niveau examen (/50)

Exercice 6 : Lire du code faux : trouver le bug et le prouver

Les deux fonctions ci-dessous sont censées calculer la moyenne d'une liste de notes et compter combien dépassent cette moyenne. Elles s'exécutent sans message d'erreur, et pourtant les deux donnent un résultat faux. On teste avec notes = [12, 15, 9, 18].

Python
def moyenne(t):
    s = 0
    for i in range(1, len(t)):
        s = s + t[i]
    return s / len(t)

def au_dessus(t):
    n = 0
    for x in t:
        if x > moyenne(t):
            n = n + 1
    return n

notes = [12, 15, 9, 18]
  • a) Calculez à la main la valeur correcte de la moyenne de notes.
  • b) Que renvoie réellement moyenne(notes) ? Identifiez précisément le bug et corrigez-le.
  • c) Une fois moyenne corrigée, que renvoie au_dessus(notes) ? Ce résultat est-il correct ?
  • d) La fonction au_dessus contient un défaut qui n'est pas un bug de résultat mais un défaut de conception. Lequel, et comment le corriger ?
  • e) Proposez un jeu de trois tests, écrits avec assert, qui aurait détecté le bug de la question b. Justifiez le choix de chaque test.
Voir la correction

a) La somme vaut 12 + 15 + 9 + 18 = 54, et il y a 4 notes : la moyenne correcte est 54 / 4 = 13,5.

b) La fonction renvoie 10,5. Le bug est dans range(1, len(t)) : la boucle démarre à l'indice 1 et saute donc le premier élément. Elle additionne 15 + 9 + 18 = 42, puis divise par 4, d'où 10,5. La correction est d'écrire range(len(t)), ou mieux, de parcourir directement les valeurs avec for x in t, ce qui rend l'erreur d'indice impossible.

c) Avec la moyenne corrigée à 13,5, les notes strictement supérieures sont 15 et 18 : au_dessus renvoie 2, ce qui est correct.

d) Le défaut est que moyenne(t) est recalculée à CHAQUE tour de boucle, alors qu'elle ne change jamais. Sur une liste de n notes, on fait n calculs de moyenne coûtant chacun n additions, soit un travail proportionnel à n2n^{2} au lieu de nn. La correction est de calculer la moyenne une seule fois avant la boucle et de la stocker dans une variable. C'est un réflexe attendu à l'examen : sortir de la boucle tout calcul qui n'en dépend pas.

e) Trois tests qui couvrent des cas différents : une liste à un seul élément, où la moyenne doit valoir cet élément (ce test échoue immédiatement avec le bug, car la boucle ne fait aucun tour et renvoie 0) ; une liste de valeurs toutes égales, où la moyenne doit valoir cette valeur commune ; et un cas connu calculé à la main. Le premier est le plus utile : les bugs d'indice se révèlent presque toujours sur les cas de taille 0 ou 1, jamais sur les cas moyens.

Python
def moyenne(t):
    s = 0
    for x in t:          # plus d'indice, plus de bug possible
        s = s + x
    return s / len(t)

def au_dessus(t):
    m = moyenne(t)       # calculee UNE fois
    n = 0
    for x in t:
        if x > m:
            n = n + 1
    return n

assert moyenne([7]) == 7
assert moyenne([5, 5, 5]) == 5
assert moyenne([12, 15, 9, 18]) == 13.5

Exercice 7 : Spécifier, tester, et le cas limite qu'on oublie toujours

On veut écrire une fonction indice_max(t) qui renvoie l'INDICE du plus grand élément d'une liste de nombres. Avant d'écrire une seule ligne, on spécifie.

  • a) Écrivez la spécification complète : ce que la fonction prend en entrée, ce qu'elle renvoie, et la précondition sur t.
  • b) Que doit renvoyer la fonction si le maximum apparaît plusieurs fois ? Complétez la spécification pour lever cette ambiguïté.
  • c) Écrivez la fonction. Elle ne doit parcourir la liste qu'une seule fois.
  • d) Un camarade initialise son maximum avec m = 0 au lieu de m = t[0]. Sur quel type de liste sa version échoue-t-elle ? Donnez un contre-exemple précis avec le résultat attendu et le résultat obtenu.
  • e) Écrivez un jeu de tests avec assert couvrant : le cas général, la liste à un élément, les valeurs négatives, et le maximum en double.
Voir la correction

a) Entrée : une liste t de nombres. Sortie : un entier i tel que t[i] soit supérieur ou égal à tous les éléments de t. Précondition : t doit être non vide. Sur une liste vide, il n'existe aucun indice à renvoyer ; on choisit d'exiger la précondition plutôt que d'inventer une valeur de retour, et on peut la matérialiser dans le code par assert len(t) > 0.

b) La spécification doit trancher : on convient de renvoyer le PREMIER indice où le maximum est atteint. Sans cette précision, deux implémentations correctes peuvent donner des réponses différentes sur [3, 9, 9], et un test automatique deviendrait arbitraire. Une spécification qui laisse une ambiguïté est une spécification incomplète.

c) On mémorise l'indice du meilleur élément vu jusqu'ici, et on ne le remplace que sur une inégalité STRICTE, ce qui garantit qu'on conserve la première occurrence.

d) Sa version échoue sur toute liste dont tous les éléments sont strictement négatifs. Sur t = [-5, -2, -9], aucun élément ne dépasse 0 : la condition x > m n'est jamais vraie, l'indice n'est jamais mis à jour et la fonction renvoie 0, donc l'indice de -5. Le résultat attendu est 1, l'indice de -2. La leçon générale : on initialise un maximum avec un élément de la liste, jamais avec une constante choisie arbitrairement.

e) Le jeu de tests figure dans le corrigé ci-dessous. Chaque test vise un risque identifié : le cas général vérifie la logique, la liste à un élément vérifie les bornes de la boucle, les négatifs attrapent le bug d'initialisation de la question d, et le doublon vérifie la convention fixée en b.

Python
def indice_max(t):
    """Plus petit indice du maximum. Precondition : t non vide."""
    assert len(t) > 0, 't ne doit pas etre vide'
    imax = 0
    for i in range(1, len(t)):
        if t[i] > t[imax]:      # strict : on garde la 1re occurrence
            imax = i
    return imax

assert indice_max([3, 9, 2]) == 1        # cas general
assert indice_max([7]) == 0              # un seul element
assert indice_max([-5, -2, -9]) == 1     # que des negatifs
assert indice_max([3, 9, 9]) == 1        # maximum en double

Exercice 8 : Tableaux à deux dimensions

On représente une grille de nombres par une liste de listes. On travaille sur la grille suivante, à 3 lignes et 3 colonnes : grille = [[3, 8, 1], [4, 2, 9], [7, 5, 6]].

  • a) Que vaut grille[1][2] ? Et grille[2][0] ? Donnez l'expression qui vaut 8.
  • b) Écrivez une fonction somme_grille(g) qui renvoie la somme de tous les éléments. Donnez le résultat sur la grille ci-dessus.
  • c) Écrivez une fonction somme_colonne(g, j) qui renvoie la somme de la colonne j. Donnez la somme de la colonne 0.
  • d) Écrivez une fonction transposee(g) qui renvoie la grille transposée (les lignes deviennent les colonnes). Donnez le résultat sur la grille ci-dessus.
  • e) On veut créer une grille de n lignes et p colonnes remplie de zéros. Expliquez pourquoi g = [[0] * p] * n est un piège, et donnez la bonne écriture.
Voir la correction

a) grille[1][2] vaut 9 : ligne d'indice 1 (la deuxième), colonne d'indice 2 (la troisième). grille[2][0] vaut 7. La valeur 8 s'obtient par grille[0][1]. L'ordre est toujours [ligne][colonne].

b) On parcourt chaque ligne, puis chaque élément de la ligne. La somme vaut 3+8+1+4+2+9+7+5+6 = 45.

c) On fixe la colonne et on fait varier l'indice de ligne. La colonne 0 contient 3, 4 et 7 : sa somme vaut 14.

d) La transposée est [[3, 4, 7], [8, 2, 5], [1, 9, 6]] : l'élément de la ligne i colonne j vient de la ligne j colonne i.

e) [0] * p crée bien une ligne de p zéros, mais la multiplication suivante par n ne duplique pas cette ligne : elle recopie n fois la MÊME référence. Les n lignes sont donc un seul et même objet, et g[0][0] = 1 met un 1 en tête de toutes les lignes à la fois. La bonne écriture crée une ligne neuve à chaque tour, par exemple en compréhension : [[0] * p for _ in range(n)]. C'est exactement le même piège que l'argument par défaut mutable de l'exercice 4 : en Python, dupliquer une référence n'est pas dupliquer l'objet.

Python
def somme_grille(g):
    s = 0
    for ligne in g:
        for x in ligne:
            s = s + x
    return s                      # 45

def somme_colonne(g, j):
    return sum(g[i][j] for i in range(len(g)))    # colonne 0 -> 14

def transposee(g):
    n, p = len(g), len(g[0])
    return [[g[i][j] for i in range(n)] for j in range(p)]

g = [[0] * p for _ in range(n)]   # correct
# g = [[0] * p] * n               # PIEGE : n fois la meme ligne

Exercice 9 : Traitement de chaînes : palindromes et chiffre de César

Les chaînes de caractères se parcourent comme des listes, mais elles sont immuables : on ne peut pas modifier un caractère en place, on construit une nouvelle chaîne. On note alpha = 'abcdefghijklmnopqrstuvwxyz'.

  • a) Écrivez une fonction est_palindrome(m) qui dit si m se lit pareil dans les deux sens. Testez sur 'radar', 'kayak', 'python' et 'ressasser'.
  • b) Écrivez la même fonction SANS utiliser de tranche, avec une boucle qui compare les caractères deux à deux depuis les extrémités. Combien de tours fait votre boucle sur un mot de n lettres ?
  • c) Le chiffre de César décale chaque lettre de k positions dans l'alphabet, en revenant au début après z. Écrivez cesar(m, k) pour un mot en minuscules sans accent. Que donne cesar('bonjour', 3) ?
  • d) Que donne cesar('xyz', 3) ? Quelle opération assure le retour au début de l'alphabet ?
  • e) Comment déchiffre-t-on un message chiffré avec la clé k ? Vérifiez sur votre réponse de la question c.
Voir la correction

a) Il suffit de comparer le mot à son renversé : return m == m[::-1]. Résultats : 'radar' vrai, 'kayak' vrai, 'python' faux, 'ressasser' vrai.

b) On avance deux indices l'un vers l'autre depuis les deux bouts, et on s'arrête dès qu'ils se croisent ou qu'une paire diffère. La boucle fait au plus n // 2 tours : une fois arrivé au milieu, toutes les paires ont été vérifiées, continuer reviendrait à recomparer les mêmes lettres. Cette version est préférable sur de longs textes, car la version avec tranche construit une copie complète du mot en mémoire.

c) On repère la position de chaque lettre dans alpha, on ajoute k, on prend le reste modulo 26 et on relit la lettre correspondante. cesar('bonjour', 3) donne 'erqmrxu'.

d) cesar('xyz', 3) donne 'abc'. C'est le modulo 26 qui assure le rebouclage : la position de 'x' est 23, plus 3 donne 26, et 26 % 26 vaut 0, soit la position de 'a'. Sans ce modulo, l'indice sortirait de l'alphabet et provoquerait une IndexError.

e) Déchiffrer, c'est chiffrer avec la clé opposée : cesar(message, -k). Le modulo gère aussi les valeurs négatives en Python, puisque le reste y est toujours positif. On vérifie : cesar('erqmrxu', -3) redonne bien 'bonjour'.

Python
alpha = 'abcdefghijklmnopqrstuvwxyz'

def est_palindrome(m):
    return m == m[::-1]

def est_palindrome2(m):
    i, j = 0, len(m) - 1
    while i < j:
        if m[i] != m[j]:
            return False
        i, j = i + 1, j - 1
    return True

def cesar(m, k):
    r = ''
    for c in m:
        r = r + alpha[(alpha.index(c) + k) % 26]
    return r

# cesar('bonjour', 3) -> 'erqmrxu'   ;   cesar('erqmrxu', -3) -> 'bonjour'

Exercice 10 : Problème : analyser un relevé de températures

Une station météo enregistre une température par jour. On travaille sur une semaine : temps = [12, 15, 14, 19, 22, 21, 17]. Toutes les fonctions demandées prennent la liste en argument et doivent fonctionner quelle que soit sa longueur, du moment qu'elle est non vide.

  • a) Écrivez amplitude(t) qui renvoie l'écart entre la température maximale et la minimale. Donnez le résultat.
  • b) Écrivez jours_au_dessus(t) qui renvoie la LISTE DES INDICES des jours dont la température dépasse strictement la moyenne. Donnez le résultat.
  • c) Écrivez plus_forte_hausse(t) qui renvoie la plus grande hausse observée d'un jour au suivant, et l'indice du jour où elle commence.
  • d) Écrivez plus_longue_hausse(t) qui renvoie la longueur de la plus longue suite de jours consécutifs où la température augmente strictement. Donnez le résultat.
  • e) Pourquoi la question d ne peut-elle pas se traiter par une simple compréhension de liste, alors que les questions b et c le peuvent ?
Voir la correction

a) amplitude(t) = max(t) - min(t) = 22 - 12 = 10 degrés.

b) La moyenne vaut 12+15+14+19+22+21+177=120717,14\frac{12+15+14+19+22+21+17}{7}=\frac{120}{7}\approx 17{,}14. Les températures qui la dépassent strictement sont 19, 22 et 21, aux indices 3, 4 et 5. La fonction renvoie donc [3, 4, 5]. Attention à calculer la moyenne UNE fois avant la compréhension, pas à l'intérieur.

c) Les écarts d'un jour au suivant sont [3, -1, 5, 3, -1, -4]. La plus forte hausse vaut 5, et elle commence à l'indice 2 (du jour 2 au jour 3, de 14 à 19).

d) Les suites strictement croissantes sont 12, 15 (longueur 2), puis 14, 19, 22 (longueur 3), puis 17 seul. La plus longue a donc une longueur de 3.

e) Parce que la question d demande de retenir une information ENTRE les tours : la longueur de la série en cours, qu'il faut remettre à 1 dès que la croissance s'interrompt, tout en conservant le meilleur score vu jusque-là. Une compréhension de liste ne peut pas porter cet état accumulé, elle calcule chaque terme indépendamment des autres. Les questions b et c s'y prêtent au contraire parce que chaque terme s'y calcule à partir des seules données locales, sans mémoire. C'est le critère à retenir : dès qu'il faut un accumulateur qui dépend de l'historique, il faut une vraie boucle.

Python
def amplitude(t):
    return max(t) - min(t)                       # 10

def jours_au_dessus(t):
    m = sum(t) / len(t)                          # calculee UNE fois
    return [i for i in range(len(t)) if t[i] > m]  # [3, 4, 5]

def plus_forte_hausse(t):
    ecarts = [t[i + 1] - t[i] for i in range(len(t) - 1)]
    h = max(ecarts)
    return h, ecarts.index(h)                    # (5, 2)

def plus_longue_hausse(t):
    best = courant = 1
    for i in range(1, len(t)):
        courant = courant + 1 if t[i] > t[i - 1] else 1
        if courant > best:
            best = courant
    return best                                  # 3

Voir aussi

Vous cherchez un tuteur en NSI à Montréal ?

Contactez-moi pour une première séance. Bachelier en informatique de McGill et maîtrise en informatique appliquée de Concordia, je travaille la NSI sur du code réel, pas seulement sur le cours.

Site par Studio Squalli