[E.5]
Mise au point des programmes, gestion des bugsDans la pratique de la programmation, savoir répondre aux causes typiques de bugs : problèmes liés au typage, effets de bord non désirés, débordement dans les tableaux, instruction conditionnelle non exhaustive, choix des inégalités, comparaisons et calculs entre flottants, mauvais nommage des variables, etc.
On prolonge le travail entrepris en classe de première sur l’utilisation de la spécification, des assertions, de la documentation des programmes et de la construction de jeux de tests. Les élèves apprennent progressivement à anticiper leurs erreurs.
Python est dynamiquement typé : une variable peut changer de type → source d’erreurs silencieuses
age = input("Âge ? ") # age est une str !
print(age + 1) # TypeError: can only concatenate str (not "int") to str
# Correction :
print(int(age) + 1)total = 0
for val in [10, "20", "30"]:
total += val # TypeError à la 2ère itération
# Correction : total += int(val)i = 42 # variable importante
for i in range(5): # i est écrasé !
print(i)
print(i) # affiche 4, pas 42
# Correction : choisir un autre nom de variableUn effet de bord : une fonction modifie quelque chose en dehors de son périmètre.
def ajouter(val, liste=[]): # PIÈGE : [] créé une seule fois
liste.append(val)
return liste
print(ajouter(1)) # [1]
print(ajouter(2)) # [1, 2] ← pas [2] !
# Correction :
def ajouter(val, liste=None):
if liste is None:
liste = []
liste.append(val)
return listedef doubler(lst):
for i in range(len(lst)):
lst[i] *= 2 # modifie la liste
return lst
notes = [10, 15, 8]
dnotes = doubler(notes)
print(notes) # [20, 30, 16] — notes a été modifié !
# Correction : travailler sur une copie
def doubler(lst):
return [x * 2 for x in lst]IndexErrorn, les indices valides
sont 0 à n-1def tri_bulles(lst):
n = len(lst)
for i in range(n):
for j in range(n - i): # BUG : 1ère itération
if lst[j] > lst[j + 1]:
lst[j], lst[j + 1] = lst[j + 1], lst[j]
# Correction : réduire la borne intérieure
for j in range(n - 1 - i): # on ne déborde pasif/elif sans else peut ignorer
silencieusement des casdef mention(note):
if note >= 16:
return "Très bien"
elif note >= 14:
return "Bien"
elif note >= 12:
return "Assez bien"
# Oubli : que retourne la fonction pour note < 12 ?
# → retourne None implicitement
print(mention(8)) # None — pas d'erreur, mais résultat faux !
# Correction : ajouter un else
else:
return "Passable ou insuffisant"else final (ou lever une exception
explicite)Les flottants sont des approximations (norme IEEE
754) → jamais comparer avec ==
0.1 + 0.2 == 0.3 # False !
print(0.1 + 0.2) # 0.30000000000000004EPSILON = 1e-9
abs((0.1 + 0.2) - 0.3) < EPSILON # True
# Ou avec le module math :
import math
math.isclose(0.1 + 0.2, 0.3) # Truetotal = 0.0
for _ in range(1000):
total += 0.1
print(total) # 99.9999999999986... ≠ 100.0decimal.Decimal pour les calculs financiers
précis ou bien rester en entier (ex : stocker les prix en centièmes de
centimes)# Illisible, non typé
def f(l):
r = 0
for x in l:
r += x
return r / len(l)
# Lisible, typé
def moyenne(notes: list[float]) -> float:
total = 0.0
for note in notes:
total += note
return total / len(notes)snake_case pour variables
et fonctions, MAJUSCULES pour constantesl, O, I (confusion
avec 1 et 0)def aire_rectangle(largeur: float, hauteur: float) -> float:
return largeur * hauteur
age: int = 17
nom: str = "Alice"X | Y : la valeur peut être de type X
ou Y (Python ≥ 3.10)X | None : valeur optionnelle (peut être absente)def premier(lst: list[int]) -> int | None:
return lst[0] if lst else None
def age_humain(age: int | float) -> str:
return f"{int(age)} ans"
# Ancienne syntaxe (Python < 3.10) :
from typing import Optional, Union
def premier(lst: list[int]) -> Optional[int]: ...
def age_humain(age: Union[int, float]) -> str: ...notes: list[float] = [12.5, 15.0, 9.0]
coords: tuple[float, float] = (48.85, 2.35) # taille fixe
rgb: tuple[int, ...] = (255, 128, 0) # taille variable
identifiants: set[int] = {1, 2, 3}tuple[X, Y] : tuple de taille exacte 2, premier élément
X, second Ytuple[X, ...] : tuple de taille quelconque, tous les
éléments de type X# dict simple
scores: dict[str, int] = {"Alice": 17, "Bob": 14}
# valeurs hétérogènes → TypedDict
from typing import TypedDict
class Eleve(TypedDict):
nom: str
age: int
notes: list[float]
alice: Eleve = {"nom": "Alice", "age": 17, "notes": [15.0, 12.5]}dict[str, int] : clés str, valeurs
intTypedDict : dictionnaire avec des clés et types fixes →
détection d’erreurs de cléfrom typing import Literal, Callable
# Literal : valeur parmi un ensemble fini
Direction = Literal["nord", "sud", "est", "ouest"]
def avancer(d: Direction) -> None: ...
avancer("nord") # OK
avancer("haut") # erreur détectée par mypy/pyright
# Callable : type d'une fonction
def appliquer(f: Callable[[float], float], x: float) -> float:
return f(x)
appliquer(abs, -3.0) # OKmypy : vérificateur de types en ligne de commandepyright / basedpyright : plus rapide,
intégré dans VS Code (Pylance)# fichier : exemple.py
def carre(n: int) -> int:
return n * n
print(carre("3")) # erreur de type$ mypy exemple.py
exemple.py:4: error: Argument 1 to "carre" has incompatible type
"str"; expected "int"strict dans les paramètresdef ou
classhelp() et les outils de documentation
automatiquedef carre(n: float) -> float:
"""Retourne le carré de n."""
return n ** 2def diviser(a: float, b: float) -> float:
"""Divise a par b.
:param a: le numérateur.
:param b: le dénominateur (doit être non nul).
:returns: le résultat de la division a / b.
:raises ValueError: si b vaut 0.
"""
if b == 0:
raise ValueError("Division par zéro")
return a / bAnnoter les types des fonctions suivantes :
def est_pair(n):
return n % 2 == 0
def concatener(mots, separateur):
return separateur.join(mots)
def maximum(a, b):
return a if a > b else b
def inverser(lst):
return [lst[i] for i in range(len(lst) - 1, -1, -1)]def est_pair(n: int) -> bool:
return n % 2 == 0
def concatener(mots: list[str], separateur: str) -> str:
return separateur.join(mots)
def maximum(a: float, b: float) -> float:
return a if a > b else b
def inverser(lst: list[int]) -> list[int]:
return [lst[i] for i in range(len(lst) - 1, -1, -1)]def have_gmail(u):
return u.get("email", "").endswith("@gmail.com")
def display_user(u, formel=False):
if formel:
return f"M./Mme {u['nom'].upper()}"
return f"{u['prenom']} {u['nom'].upper()}"
user = {"nom": "Dupont", "prenom": "Alice"}
print(have_gmail(user)) # False
print(display_user(user)) # Alice DUPONT
print(display_user(user, True)) # M./Mme DUPONT from typing import TypedDict, NotRequired
class Utilisateur(TypedDict):
nom: str
prenom: str
email: NotRequired[str]
def have_gmail(u: Utilisateur) -> bool:
return u.get("email", "").endswith("@gmail.com")
def display_user(u: Utilisateur, formel: bool = False) -> str:
if formel:
return f"M./Mme {u['nom'].upper()}"
return f"{u['prenom']} {u['nom'].upper()}"
user: Utilisateur = {"nom": "Dupont", "prenom": "Alice"}
print(have_gmail(user)) # False
print(display_user(user)) # Alice DUPONT
print(display_user(user, True)) # M./Mme DUPONTTypedDict : définit la structure attendue du
dictionnaire uNotRequired[str] : la clé email peut être
absente → utiliser .get() pour y accéderformel: bool = False : argument optionnel avec valeur
par défautassert condition, message : lève
AssertionError si la condition est faussedef racine(n: float) -> float:
assert n >= 0, f"n doit être positif, reçu : {n}"
return n ** 0.5
racine(-1) # AssertionError: n doit être positif, reçu : -1pytest : bibliothèque standard de tests en Pythonuv add pytest /
pip install pytesttest_*.py, fonctions
test_*()# fichier : test_chaines.py
from chaines import inverser, concatener
def test_inverser_liste():
assert inverser([1, 2, 3]) == [3, 2, 1]
def test_inverser_un_element():
assert inverser([42]) == [42]
def test_concatener():
assert concatener(["a", "b", "c"], "-") == "a-b-c"$ pytest test_chaines.py
... # 3 tests passés
3 passed in 0.05simport pytest
def test_inverser_vide():
assert inverser([]) == []
def test_concatener_separateur_vide():
assert concatener(["a", "b"], "") == "ab"pytest --cov : mesure le % de lignes exécutées par les
testsclass Pile:
def __init__(self):
self._data = []
def empiler(self, val):
self._data.append(val)
def depiler(self):
if self.est_vide():
raise IndexError("Pile vide")
return self._data.pop()
def sommet(self):
if self.est_vide():
raise IndexError("Pile vide")
return self._data[-1]
def est_vide(self):
return len(self._data) == 0Écrire un fichier test_pile.py testant : pile vide,
empilement, dépilement, sommet, et les erreurs sur pile vide.
# fichier : test_pile.py
import pytest
from pile import Pile
def test_pile_vide_au_depart():
p = Pile()
assert p.est_vide()
def test_empiler_rend_non_vide():
p = Pile()
p.empiler(1)
assert not p.est_vide()
def test_sommet_apres_empilement():
p = Pile()
p.empiler(10)
p.empiler(20)
assert p.sommet() == 20 # sommet ne dépile pas
def test_depiler_ordre_lifo():
p = Pile()
p.empiler(1)
p.empiler(2)
p.empiler(3)
assert p.depiler() == 3
assert p.depiler() == 2
def test_depiler_vide_leve_erreur():
p = Pile()
with pytest.raises(IndexError):
p.depiler()
def test_sommet_vide_leve_erreur():
p = Pile()
with pytest.raises(IndexError):
p.sommet()_data)