Exercices : mise au point et débogage

Exercice 1 : QCM

Pour chaque question, une seule réponse est correcte.

1. Quelle instruction Python permet de vérifier une condition et d’interrompre le programme si elle est fausse ?

  • A. try condition
  • B. raise condition
  • C. assert condition
  • D. if not condition: exit()
Correction

C. assert condition vérifie la condition et lève une AssertionError si elle est fausse. A est la syntaxe de gestion d’exceptions. B lève une exception mais ne vérifie pas de condition booléenne. D fonctionnerait mais ce n’est pas le mécanisme standard de Python pour les préconditions.

2. Pourquoi la condition while lst[i] < 100 and i < len(lst) est-elle dangereuse ?

  • A. len(lst) recalcule la longueur à chaque itération
  • B. La condition devrait utiliser or au lieu de and
  • C. Il manque une instruction break
  • D. Python accède à lst[i] avant de vérifier que i est un indice valide
Correction

D. Python évalue les conditions de gauche à droite. Si i >= len(lst), l’accès lst[i] provoque une IndexError avant que la seconde condition ne soit testée. Il faut écrire i < len(lst) and lst[i] < 100 pour bénéficier de l’évaluation paresseuse du and : si la première condition est fausse, la seconde n’est pas évaluée.

3. Que vaut 0.1 + 0.2 == 0.3 en Python ?

  • A. True
  • B. False
  • C. None
  • D. Une erreur est levée
Correction

B. En raison de la représentation des nombres flottants en binaire (norme IEEE 754), 0.1 + 0.2 vaut 0.30000000000000004, qui n’est pas exactement égal à 0.3. C’est un piège classique de la programmation. Pour comparer des flottants, on utilise une tolérance : abs(a - b) < epsilon.

4. Quel est le résultat de ce code ?

x = [1, 2, 3]
y = x
y.append(4)
print(len(x))
  • A. 3
  • B. 4
  • C. Une erreur AttributeError
  • D. Une erreur TypeError
Correction

B. L’instruction y = x ne crée pas une copie : y et x référencent le même objet en mémoire. Modifier y modifie aussi x. C’est un effet de bord fréquent. Pour créer une copie indépendante, écrire y = x[:] ou y = list(x).


Exercice 2 : identifier les erreurs

Pour chaque extrait de code, identifiez le type d’erreur que Python va lever et expliquez pourquoi.

a)

def saluer(nom)
    print(f"Bonjour {nom}")

b)

notes = [15, 12, 18]
print(notes[3])

c)

age = input("Votre âge : ")
annee_naissance = 2026 - age

d)

def diviser(a, b):
    return a / b

print(diviser(10, 0))

e)

dico = {"a": 1, "b": 2}
print(dico["c"])
Correction

a) SyntaxError. Il manque les deux-points : à la fin de la ligne def saluer(nom). La syntaxe correcte est def saluer(nom):.

b) IndexError: list index out of range. La liste notes contient trois éléments aux indices 0, 1 et 2. L’indice 3 n’existe pas : c’est un débordement de tableau.

c) TypeError. La fonction input renvoie toujours une chaîne de caractères. On tente de soustraire une chaîne d’un entier : c’est une erreur de typage. Correction : age = int(input("Votre âge : ")).

d) ZeroDivisionError. Division par zéro. Il faudrait vérifier que b != 0 ou gérer l’exception.

e) KeyError: 'c'. La clé "c" n’existe pas dans le dictionnaire. Pour éviter cette erreur, utiliser dico.get("c", valeur_par_defaut).


Exercice 3 : tracer l’exécution avec print (exercice guidé)

La fonction suivante est censée renvoyer l’indice du maximum d’une liste, et l’on veut vérifier son comportement sur des cas limites.

def indice_max(L):
    i_max = 0
    for i in range(len(L)):
        if L[i] > L[i_max]:
            i_max = i
    return i_max

a) Testez indice_max([3, 7, 2, 9, 1]). Quel résultat obtenez-vous ? Est-il correct ?

b) Testez indice_max([5, 5, 5]). Quel résultat obtenez-vous ? Est-ce le comportement attendu ?

c) Ajoutez des print dans la boucle pour tracer les valeurs de i, L[i], i_max et L[i_max] à chaque itération. Exécutez sur [3, 7, 2, 9, 1] et copiez la trace.

d) Testez maintenant indice_max([9, 1, 9, 1]). Quel indice est renvoyé ? Si l’on souhaite renvoyer le dernier indice du maximum (et non le premier), quelle modification faut-il apporter ?

Correction

a) indice_max([3, 7, 2, 9, 1]) renvoie 3, ce qui est correct (le maximum 9 est à l’indice 3).

b) indice_max([5, 5, 5]) renvoie 0. Avec des éléments tous égaux, la condition L[i] > L[i_max] est toujours fausse (strictement supérieur), donc i_max reste à 0. C’est un comportement correct si l’on veut le premier indice du maximum.

c) Avec les print ajoutés :

def indice_max(L):
    i_max = 0
    for i in range(len(L)):
        print(f"i={i}, L[i]={L[i]}, i_max={i_max}, L[i_max]={L[i_max]}")
        if L[i] > L[i_max]:
            i_max = i
            print(f"  → nouveau max trouvé, i_max={i_max}")
    return i_max

Trace pour [3, 7, 2, 9, 1] :

i=0, L[i]=3, i_max=0, L[i_max]=3
i=1, L[i]=7, i_max=0, L[i_max]=3
  → nouveau max trouvé, i_max=1
i=2, L[i]=2, i_max=1, L[i_max]=7
i=3, L[i]=9, i_max=1, L[i_max]=7
  → nouveau max trouvé, i_max=3
i=4, L[i]=1, i_max=3, L[i_max]=9

La trace montre que la fonction fonctionne correctement sur cet exemple.

d) indice_max([9, 1, 9, 1]) renvoie 0 (le premier 9). Pour renvoyer le dernier, il faut changer > en >= : if L[i] >= L[i_max]. Ainsi, quand on rencontre un élément égal au maximum actuel, on met à jour i_max.


Exercice 4 : les pièges des flottants

a) Exécutez les expressions suivantes en Python et notez les résultats :

0.1 + 0.2
0.1 + 0.2 == 0.3
1.0 + 1e-16 == 1.0

Expliquez chaque résultat.

b) La fonction suivante est censée vérifier si un triangle est rectangle en utilisant le théorème de Pythagore. Pourquoi peut-elle échouer avec certaines valeurs ?

def est_rectangle(a, b, c):
    """Vérifie si un triangle de côtés a, b, c est rectangle."""
    cotes = sorted([a, b, c])
    return cotes[0]**2 + cotes[1]**2 == cotes[2]**2

c) Réécrivez la fonction en utilisant une comparaison avec tolérance. On utilisera abs(x - y) < epsilon avec epsilon = 1e-9.

d) Testez les deux versions avec le triangle de côtés $0.5$, $1.2$ et $1.3$. Que constatez-vous ?

Correction

a)

  • 0.1 + 0.2 affiche 0.30000000000000004. Le nombre 0.1 n’est pas représentable exactement en binaire ; l’addition accumule l’erreur d’arrondi.
  • 0.1 + 0.2 == 0.3 vaut False. La somme n’est pas exactement 0.3 en raison des erreurs de représentation.
  • 1.0 + 1e-16 == 1.0 vaut True. Le nombre $10^{-16}$ est trop petit par rapport à 1.0 pour être représenté dans la somme (les flottants ont environ 15 à 16 chiffres significatifs).

b) La comparaison avec == entre flottants est dangereuse. cotes[0]**2 + cotes[1]**2 et cotes[2]**2 peuvent différer de quelques $10^{-16}$ même pour un vrai triangle rectangle, à cause des erreurs d’arrondi.

c)

def est_rectangle(a, b, c, epsilon=1e-9):
    """Vérifie si un triangle est rectangle (avec tolérance)."""
    cotes = sorted([a, b, c])
    return abs(cotes[0]**2 + cotes[1]**2 - cotes[2]**2) < epsilon

d) Avec $a = 0.5$, $b = 1.2$, $c = 1.3$ :

  • Version avec == : 0.5**2 + 1.2**2 = 1.69 et 1.3**2 = 1.6900000000000002. Le test == renvoie False alors que le triangle est rectangle ($0.25 + 1.44 = 1.69$).
  • Version avec tolérance : abs(1.69 - 1.6900000000000002) ≈ 2.2e-16 < 1e-9, donc le test renvoie True.

Exercice 5 : écrire des assertions et des tests

a) Écrire des assertions de précondition pour la fonction moyenne(L) : la liste ne doit pas être vide et tous ses éléments doivent être des nombres.

b) Compléter les docstrings de la fonction suivante avec des doctests couvrant les cas normaux et les cas limites :

def est_palindrome(s):
    """
    Renvoie True si la chaîne s est un palindrome, False sinon.

    >>> # Écrire les doctests ici
    """
    return s == s[::-1]

c) Pour la fonction tri_insertion(L), proposer au moins quatre jeux de tests pertinents (liste quelconque, liste vide, liste déjà triée, liste avec doublons) et justifier le choix de chacun.

Correction

a)

def moyenne(L):
    assert len(L) > 0, "La liste ne doit pas être vide"
    assert all(isinstance(x, (int, float)) for x in L), \
        "Tous les éléments doivent être des nombres"
    return sum(L) / len(L)

b)

def est_palindrome(s):
    """
    Renvoie True si la chaîne s est un palindrome, False sinon.

    >>> est_palindrome("kayak")
    True
    >>> est_palindrome("python")
    False
    >>> est_palindrome("")
    True
    >>> est_palindrome("a")
    True
    >>> est_palindrome("ab")
    False
    """
    return s == s[::-1]

Cas testés : palindrome classique, non-palindrome, chaîne vide (cas limite), un caractère, deux caractères différents.

c) Jeux de tests pour tri_insertion :

  1. Liste quelconque [5, 3, 8, 1, 9][1, 3, 5, 8, 9] : teste le fonctionnement normal ;
  2. Liste vide [][] : cas limite, vérifie qu’il n’y a pas d’erreur d’indice ;
  3. Liste déjà triée [1, 2, 3, 4][1, 2, 3, 4] : meilleur cas, vérifie que le tri ne modifie pas inutilement ;
  4. Liste en ordre inverse [5, 4, 3, 2, 1][1, 2, 3, 4, 5] : pire cas pour le tri par insertion ;
  5. Liste avec doublons [3, 1, 4, 1, 5][1, 1, 3, 4, 5] : vérifie que les éléments égaux sont correctement traités.

Exercice 6 : chasse aux bugs

Chacun des programmes suivants contient un bug relevant d’une cause classique. Identifiez l’erreur, nommez sa cause et proposez une correction.

a) Indentation incorrecte :

def compter_voyelles(mot):
    voyelles = "aeiouy"
    compteur = 0
    for i in range(len(mot)):
        if mot[i] in voyelles:
            compteur += 1
        return compteur

b) Effet de bord :

def inverser_liste(L):
    resultat = L
    resultat.reverse()
    return resultat

ma_liste = [1, 2, 3]
inversee = inverser_liste(ma_liste)
print(ma_liste)

c) Choix d’inégalité :

def somme_entiers(n):
    s = 0
    i = 1
    while i < n:
        s += i
        i += 1
    return s

print(somme_entiers(5))  # Attendu : 15, obtenu : ?

d) Conditionnelle non exhaustive :

def mention(moyenne):
    if moyenne >= 16:
        return "Très bien"
    elif moyenne >= 14:
        return "Bien"
    elif moyenne >= 12:
        return "Assez bien"

print(mention(10))  # Que se passe-t-il ?

e) Mauvais nommage :

list = [4, 7, 2, 9]
somme = sum(list)
list = list + [5]     # Que se passe-t-il ici ?

f) Comparaison de flottants :

x = 0.1 + 0.1 + 0.1
if x == 0.3:
    print("Égal")
else:
    print("Différent")  # Quel message s'affiche ?
Correction

a) Cause : indentation incorrecte. Le return compteur est à l’intérieur de la boucle (même niveau que le if). La fonction renvoie dès la première itération. Correction : désindenter le return pour le placer après la boucle.

    return compteur  # en dehors de la boucle for

b) Cause : effet de bord (alias). resultat = L ne crée pas de copie : les deux variables pointent vers le même objet. reverse() modifie ma_liste en place. print(ma_liste) affiche [3, 2, 1] au lieu de [1, 2, 3]. Correction : resultat = L[:] (copie).

c) Cause : choix d’inégalité (erreur de borne). while i < n exclut la valeur n. Pour $n = 5$, on obtient $1 + 2 + 3 + 4 = 10$ au lieu de $1 + 2 + 3 + 4 + 5 = 15$. Correction : while i <= n.

d) Cause : conditionnelle non exhaustive. Si la moyenne est inférieure à 12, aucune branche ne renvoie de valeur. mention(10) renvoie None (valeur par défaut quand une fonction n’a pas de return). Correction : ajouter un else: return "Passable" (ou "Insuffisant" selon les cas).

e) Cause : mauvais nommage (masquage d’un mot réservé). list est le nom d’une fonction native de Python. En écrivant list = [4, 7, 2, 9], on écrase cette fonction. L’expression list + [5] fonctionne (concaténation de listes), mais tout appel ultérieur à list() dans le programme échouera avec TypeError: 'list' object is not callable. Correction : choisir un nom différent, par exemple ma_liste ou nombres.

f) Cause : comparaison de flottants. 0.1 + 0.1 + 0.1 vaut 0.30000000000000004 en raison des erreurs d’arrondi des flottants. Le test x == 0.3 est False, donc le message « Différent » s’affiche. Correction : if abs(x - 0.3) < 1e-9.


Exercice 7 : gestion des exceptions

a) Écrire une fonction saisie_entier() qui demande à l’utilisateur de saisir un entier et redemande tant que la saisie n’est pas valide. Utiliser try/except.

b) Écrire une fonction lire_fichier(chemin) qui renvoie le contenu d’un fichier sous forme de chaîne, ou "Fichier introuvable" si le fichier n’existe pas.

c) Définir une exception personnalisée NoteInvalideError et une fonction ajouter_note(notes, n) qui lève cette exception si n n’est pas entre 0 et 20.

Correction

a)

def saisie_entier():
    while True:
        try:
            return int(input("Entrez un entier : "))
        except ValueError:
            print("Ce n'est pas un entier, réessayez.")

La boucle while True redemande tant que int() échoue. On capture spécifiquement ValueError.

b)

def lire_fichier(chemin):
    try:
        with open(chemin, "r") as f:
            return f.read()
    except FileNotFoundError:
        return "Fichier introuvable"

On capture FileNotFoundError (pas Exception, pour ne pas masquer d’autres erreurs).

c)

class NoteInvalideError(Exception):
    pass

def ajouter_note(notes, n):
    if not (0 <= n <= 20):
        raise NoteInvalideError(f"Note {n} hors limites [0, 20]")
    notes.append(n)

On peut capturer cette exception avec try/except NoteInvalideError.


Exercice 8 : synthèse – débogage complet d’un programme

Le programme suivant est censé lire une liste de notes séparées par des virgules, calculer la moyenne et afficher le résultat avec une mention. Il contient deux bugs et deux situations d’erreur non gérées. Les trouver, les corriger et ajouter une gestion d’erreur appropriée.

def calculer_moyenne(chaine):
    notes = chaine.split(",")
    somme = 0
    for note in notes:
        somme += note
    moyenne = somme / len(notes)
    if moyenne >= 10:
        mention = "admis"
    elif moyenne >= 8:
        mention = "rattrapage"
    return f"Moyenne : {moyenne}/20 ({mention})"

resultat = calculer_moyenne("12,15,8,14,9")
print(resultat)
Correction

Bug 1 : erreur de typage. chaine.split(",") renvoie des chaînes, pas des nombres. somme += note tente d’additionner l’entier 0 et une chaîne : Python lève TypeError. Correction : somme += float(note).

Bug 2 : conditionnelle non exhaustive. Si la moyenne est inférieure à 8, la variable mention n’est jamais définie. Le return provoque un NameError. Correction : ajouter else: mention = "ajourné".

Bug 3 : pas de gestion d’erreur. Si la chaîne contient un élément non numérique ("12,abc,8"), float(note) échoue. Il faut gérer ce cas.

Bug 4 : pas de gestion de la chaîne vide. Si chaine est vide, notes contient [""] et la conversion échoue.

Version corrigée :

def calculer_moyenne(chaine):
    assert chaine.strip() != "", "La chaîne ne doit pas être vide"
    parties = chaine.split(",")
    notes = []
    for p in parties:
        try:
            n = float(p.strip())
            if not (0 <= n <= 20):
                raise ValueError(f"Note hors limites : {n}")
            notes.append(n)
        except ValueError as e:
            print(f"Note ignorée ({e}) : '{p}'")
    if len(notes) == 0:
        return "Aucune note valide"
    moyenne = sum(notes) / len(notes)
    if moyenne >= 10:
        mention = "admis"
    elif moyenne >= 8:
        mention = "rattrapage"
    else:
        mention = "ajourné"
    return f"Moyenne : {moyenne:.2f}/20 ({mention})"