26. Techniques de programmation défensive avec les exceptions et la validation
La programmation défensive (defensive programming) consiste à écrire du code qui anticipe les problèmes avant qu’ils ne se produisent. Au lieu de supposer que tout fonctionnera parfaitement, le code défensif valide les entrées, gère les erreurs de manière élégante et vérifie les hypothèses. Cette approche permet de créer des programmes plus fiables, plus faciles à déboguer et moins susceptibles de planter de manière inattendue.
Dans les chapitres précédents, nous avons appris à gérer les exceptions lorsqu’elles se produisent. Maintenant, nous allons apprendre à éviter que de nombreuses erreurs ne se produisent en premier lieu, et à détecter les problèmes tôt lorsqu’ils se produisent.
26.1) Valider les arguments des fonctions
Les fonctions reçoivent souvent des données provenant d’autres parties de votre programme ou des utilisateurs. Si une fonction reçoit des données invalides, elle peut produire des résultats incorrects, planter avec une erreur déroutante, ou provoquer des problèmes ailleurs dans votre programme. La validation des arguments (argument validation) consiste à vérifier que les arguments d’une fonction respectent vos exigences avant de les utiliser.
26.1.1) Pourquoi valider les arguments ?
Considérez cette fonction qui calcule le pourcentage de note d’un étudiant :
def calculate_percentage(points_earned, total_points):
return (points_earned / total_points) * 100
# Utilisation de la fonction
percentage = calculate_percentage(85, 100)
print(f"Grade: {percentage}%") # Output: Grade: 85.0%Cela fonctionne très bien avec des entrées valides. Mais que se passe-t-il avec des données problématiques ?
# Problème 1 : division par zéro
percentage = calculate_percentage(85, 0) # ZeroDivisionError!
# Problème 2 : valeurs négatives (ça n’a pas de sens)
percentage = calculate_percentage(-10, 100) # -10.0%
# Problème 3 : points obtenus supérieurs au total (impossible)
percentage = calculate_percentage(120, 100) # 120.0%Sans validation, la fonction plante ou produit des résultats absurdes. Les messages d’erreur n’expliquent pas ce qui s’est mal passé du point de vue de la logique métier—ils ne montrent que des échecs techniques.
26.1.2) Validation de base des arguments avec des conditions
L’approche de validation la plus simple utilise des instructions if pour vérifier les arguments et lever des exceptions lorsqu’ils sont invalides :
def calculate_percentage(points_earned, total_points):
# Valider total_points
if total_points <= 0:
raise ValueError("total_points must be positive")
# Valider points_earned
if points_earned < 0:
raise ValueError("points_earned cannot be negative")
if points_earned > total_points:
raise ValueError("points_earned cannot exceed total_points")
# Toutes les validations ont réussi - on peut calculer en toute sécurité
return (points_earned / total_points) * 100
# Utilisation valide
percentage = calculate_percentage(85, 100)
print(f"Grade: {percentage}%") # Output: Grade: 85.0%
# Utilisation invalide - messages d'erreur clairs
try:
percentage = calculate_percentage(85, 0)
except ValueError as e:
print(f"Error: {e}") # Output: Error: total_points must be positive
try:
percentage = calculate_percentage(-10, 100)
except ValueError as e:
print(f"Error: {e}") # Output: Error: points_earned cannot be negative
try:
percentage = calculate_percentage(120, 100)
except ValueError as e:
print(f"Error: {e}") # Output: Error: points_earned cannot exceed total_pointsDésormais, quand quelque chose se passe mal, le message d’erreur explique clairement quel est le problème et comment le corriger.
26.1.3) Valider les types des arguments
Parfois, vous devez vous assurer que les arguments sont du bon type :
def calculate_discount(price, discount_percent):
# Valider les types
if not isinstance(price, (int, float)):
raise TypeError("price must be a number")
if not isinstance(discount_percent, (int, float)):
raise TypeError("discount_percent must be a number")
# Valider les valeurs
if price < 0:
raise ValueError("price cannot be negative")
if not (0 <= discount_percent <= 100):
raise ValueError("discount_percent must be between 0 and 100")
# Calculer la remise
discount_amount = price * (discount_percent / 100)
return price - discount_amount
# Utilisation valide
final_price = calculate_discount(50.00, 20)
print(f"Final price: ${final_price:.2f}") # Output: Final price: $40.00
# Erreur de type
try:
final_price = calculate_discount("50", 20)
except TypeError as e:
print(f"Error: {e}") # Output: Error: price must be a number
# Erreur de valeur
try:
final_price = calculate_discount(50.00, 150)
except ValueError as e:
print(f"Error: {e}") # Output: Error: discount_percent must be between 0 and 100La fonction isinstance() vérifie si un objet est une instance d’un type (ou de types) spécifié(s). Nous passons un tuple (int, float) pour accepter soit des entiers soit des flottants, puisque ce sont tous deux des types numériques valides pour des prix.
Quand valider les types : la philosophie de Python est le « duck typing »—si un objet se comporte comme ce dont vous avez besoin, utilisez-le. La validation de type est la plus utile lorsque :
- Vous écrivez une fonction qui sera utilisée par d’autres
- Des erreurs de type provoqueraient des échecs déroutants plus tard
- La fonction fait partie d’une API publique ou d’une bibliothèque
26.1.4) Valider les arguments de type collection
Lorsque des fonctions acceptent des listes, des dictionnaires, ou d’autres collections, validez à la fois la collection et son contenu :
def calculate_average_grade(grades):
# Valider la collection elle-même
if not isinstance(grades, list):
raise TypeError("grades must be a list")
if len(grades) == 0:
raise ValueError("grades list cannot be empty")
# Valider chaque note dans la collection
for i, grade in enumerate(grades):
if not isinstance(grade, (int, float)):
raise TypeError(f"grade at index {i} must be a number, got {type(grade).__name__}")
if not (0 <= grade <= 100):
raise ValueError(f"grade at index {i} must be between 0 and 100, got {grade}")
# Toutes les validations ont réussi
return sum(grades) / len(grades)
# Utilisation valide
grades = [85, 92, 78, 95]
average = calculate_average_grade(grades)
print(f"Average: {average:.1f}") # Output: Average: 87.5
# Erreur : liste vide
try:
average = calculate_average_grade([])
except ValueError as e:
print(f"Error: {e}") # Output: Error: grades list cannot be empty
# Type de note invalide
try:
average = calculate_average_grade([85, "92", 78])
except TypeError as e:
print(f"Error: {e}") # Output: Error: grade at index 1 must be a number, got str
# Valeur de note invalide
try:
average = calculate_average_grade([85, 92, 150])
except ValueError as e:
print(f"Error: {e}") # Output: Error: grade at index 2 must be between 0 and 100, got 150Remarquez comment nous incluons l’index dans les messages d’erreur lors de la validation des éléments d’une collection. Cela aide à identifier précisément quel élément pose problème, en particulier dans de grandes collections.
26.2) Vérifier la validité des entrées utilisateur
Les entrées utilisateur sont intrinsèquement peu fiables—les utilisateurs font des fautes de frappe, comprennent mal les instructions, ou saisissent des données dans des formats inattendus. Valider les entrées utilisateur empêche ces erreurs de provoquer des plantages du programme ou des résultats incorrects.
26.2.1) Modèle de base pour la validation des entrées
Le modèle fondamental de validation d’entrée combine input() avec des contrôles de validation :
# Obtenir l’entrée utilisateur
age_str = input("Enter your age: ")
# Valider l’entrée
try:
age = int(age_str)
if age < 0:
print("Error: Age cannot be negative")
elif age > 150:
print("Error: Age seems unrealistic")
else:
print(f"You are {age} years old")
except ValueError:
print("Error: Please enter a valid number")Ce modèle comporte trois parties :
- Obtenir l’entrée sous forme de chaîne
- Essayer de la convertir dans le type nécessaire
- Vérifier si la valeur convertie est valide
Voyons cela en action avec différentes entrées :
# Entrée valide
# User enters: 25
# Output: You are 25 years old
# Type invalide
# User enters: twenty-five
# Output: Error: Please enter a valid number
# Valeur invalide (négative)
# User enters: -5
# Output: Error: Age cannot be negative
# Valeur invalide (irréaliste)
# User enters: 200
# Output: Error: Age seems unrealistic26.2.2) Valider des plages et des formats d’entrée
Certaines entrées doivent se situer dans des plages spécifiques ou correspondre à des formats particuliers :
# Validation d’un mois (1-12)
month_str = input("Enter month (1-12): ")
try:
month = int(month_str)
if not (1 <= month <= 12):
print("Error: Month must be between 1 and 12")
else:
print(f"Month: {month}")
except ValueError:
print("Error: Please enter a whole number")
# Validation du format d’un email (contrôle simple)
email = input("Enter email: ")
if '@' not in email or '.' not in email:
print("Error: Email must contain @ and .")
else:
print(f"Email: {email}")
# Validation d’une entrée oui/non
response = input("Continue? (yes/no): ").lower().strip()
if response not in ['yes', 'no', 'y', 'n']:
print("Error: Please answer yes or no")
else:
if response in ['yes', 'y']:
print("Continuing...")
else:
print("Stopping...")La validation des e-mails ici est volontairement simple—elle vérifie seulement une structure de base. La validation réelle des e-mails est bien plus complexe et utilise généralement des expressions régulières (que nous apprendrons au Chapitre 39).
26.2.3) Fournir des messages d’erreur utiles
De bons messages d’erreur indiquent aux utilisateurs exactement ce qui s’est mal passé et comment le corriger :
# Mauvais message d’erreur
password = input("Enter password: ")
if len(password) < 8:
print("Error: Invalid password") # Pas utile !
# Meilleur message d’erreur
password = input("Enter password: ")
if len(password) < 8:
print("Error: Password must be at least 8 characters long")
print(f"Your password is only {len(password)} characters")
# Encore mieux - expliquer toutes les exigences dès le départ
print("Password requirements:")
print("- At least 8 characters")
print("- Must contain at least one number")
password = input("Enter password: ")
# Vérifier la longueur
if len(password) < 8:
print(f"Error: Password too short ({len(password)} characters)")
print("Password must be at least 8 characters")
# Vérifier la présence d’un chiffre
elif not any(char.isdigit() for char in password):
print("Error: Password must contain at least one number")
else:
print("Password accepted")La fonction any() renvoie True si au moins un élément d’un itérable est vrai. Ici, char.isdigit() vérifie si chaque caractère est un chiffre, et any() nous indique si au moins un caractère a réussi le test.
26.3) Combiner input(), les boucles et try/except pour une gestion robuste des entrées
Les vérifications de validation isolées sont utiles, mais elles ne suffisent pas à gérer des erreurs utilisateur répétées. Si un utilisateur saisit des données invalides, votre programme doit lui donner une autre chance. Combiner des boucles avec la validation crée une gestion robuste des entrées qui continue de demander jusqu’à obtenir des données valides.
26.3.1) Le modèle de boucle d’entrée de base
Le modèle fondamental utilise une boucle while qui continue jusqu’à ce qu’une entrée valide soit reçue :
# Continuer à demander jusqu’à obtenir un âge valide
while True:
age_str = input("Enter your age: ")
try:
age = int(age_str)
if age < 0:
print("Error: Age cannot be negative. Please try again.")
elif age > 150:
print("Error: Age seems unrealistic. Please try again.")
else:
# Entrée valide - sortir de la boucle
break
except ValueError:
print("Error: Please enter a valid number.")
print(f"You are {age} years old")Ce modèle comporte plusieurs éléments clés :
while True:crée une boucle infinie- La validation se fait à l’intérieur de la boucle
breaksort de la boucle lorsque l’entrée est valide- Les messages d’erreur encouragent l’utilisateur à réessayer
Voyons comment cela gère diverses entrées :
# Exemple d’interaction :
# Enter your age: twenty
# Error: Please enter a valid number.
# Enter your age: -5
# Error: Age cannot be negative. Please try again.
# Enter your age: 25
# You are 25 years old26.3.2) Créer des fonctions d’entrée réutilisables
Quand vous avez besoin du même type d’entrée validée à plusieurs endroits, créez une fonction :
def get_positive_integer(prompt):
"""Continuer à demander jusqu’à ce que l’utilisateur saisisse un entier positif."""
while True:
try:
value = int(input(prompt))
if value <= 0:
print("Error: Please enter a positive number.")
else:
return value
except ValueError:
print("Error: Please enter a valid whole number.")
def get_number_in_range(prompt, min_value, max_value):
"""Continuer à demander jusqu’à ce que l’utilisateur saisisse un nombre dans l’intervalle spécifié."""
while True:
try:
value = float(input(prompt))
if value < min_value or value > max_value:
print(f"Error: Please enter a number between {min_value} and {max_value}.")
else:
return value
except ValueError:
print("Error: Please enter a valid number.")
# Utilisation des fonctions
quantity = get_positive_integer("Enter quantity: ")
print(f"Quantity: {quantity}")
grade = get_number_in_range("Enter grade (0-100): ", 0, 100)
print(f"Grade: {grade}")
temperature = get_number_in_range("Enter temperature (-50 to 50): ", -50, 50)
print(f"Temperature: {temperature}°C")Ces fonctions encapsulent la logique de validation, ce qui rend votre code principal plus propre et plus lisible. Elles garantissent également un comportement de validation cohérent dans l’ensemble de votre programme.
26.4) Utiliser les assertions pour des vérifications d’invariants en phase de développement
Les assertions sont un type particulier de vérification utilisé pendant le développement pour s’assurer que les hypothèses de votre code sont correctes. Contrairement à la validation (qui gère les erreurs attendues provenant des utilisateurs ou de données externes), les assertions détectent les erreurs de programmation—des situations qui ne devraient jamais se produire si votre code est correct.
26.4.1) Ce que sont les assertions et quand les utiliser
Une assertion est une instruction qui doit toujours être vraie à un moment donné de votre code. Si elle est fausse, quelque chose ne va fondamentalement pas dans la logique de votre programme :
def calculate_average(numbers):
# Cela ne devrait jamais arriver si la fonction est appelée correctement
assert len(numbers) > 0, "numbers list cannot be empty"
return sum(numbers) / len(numbers)
# Utilisation correcte
grades = [85, 90, 78]
average = calculate_average(grades)
print(f"Average: {average:.1f}") # Output: Average: 84.3
# Utilisation incorrecte - déclenche l’assertion
empty_list = []
average = calculate_average(empty_list) # AssertionError: numbers list cannot be emptyQuand une assertion échoue, Python lève une AssertionError avec votre message. Cela arrête immédiatement le programme et vous montre exactement où votre hypothèse a été violée.
Distinction clé :
- Validation (avec
ifetraise) : pour gérer des problèmes attendus provenant des utilisateurs ou de données externes - Assertions : pour détecter des bugs de programmation pendant le développement
# Validation - gère les erreurs utilisateur attendues
def get_positive_number(prompt):
while True:
try:
value = float(input(prompt))
if value <= 0:
print("Error: Please enter a positive number.")
else:
return value
except ValueError:
print("Error: Please enter a valid number.")
# Assertion - détecte les erreurs de programmation
def calculate_discount(price, discount_rate):
# Elles ne devraient jamais être violées si le programme est écrit correctement
assert price >= 0, "price should be non-negative"
assert 0 <= discount_rate <= 1, "discount_rate should be between 0 and 1"
return price * (1 - discount_rate)26.4.2) Vérifier les préconditions d’une fonction
Les assertions sont excellentes pour vérifier que les préconditions (preconditions) (exigences qui doivent être vraies avant l’exécution de la fonction) sont respectées :
def get_list_element(items, index):
"""Obtenir un élément d’une liste à l’index spécifié."""
# Préconditions
assert isinstance(items, list), "items must be a list"
assert isinstance(index, int), "index must be an integer"
assert 0 <= index < len(items), f"index {index} out of range for list of length {len(items)}"
return items[index]
# Utilisation correcte
numbers = [10, 20, 30, 40]
value = get_list_element(numbers, 2)
print(f"Value: {value}") # Output: Value: 30
# Erreur de programmation - mauvais type
value = get_list_element("not a list", 0) # AssertionError: items must be a list
# Erreur de programmation - index invalide
value = get_list_element(numbers, 10) # AssertionError: index 10 out of range for list of length 4Ces assertions aident à détecter des bugs pendant le développement. Si vous passez accidentellement le mauvais type ou un index invalide, l’assertion vous indique immédiatement ce qui s’est mal passé.
26.4.3) Vérifier les postconditions d’une fonction
Les postconditions (postconditions) sont des conditions qui doivent être vraies après l’exécution d’une fonction. Les assertions peuvent vérifier que votre fonction a produit des résultats valides :
def calculate_percentage(part, whole):
"""Calcule le pourcentage que représente 'part' par rapport à 'whole'."""
# Préconditions
assert whole > 0, "whole must be positive"
assert part >= 0, "part must be non-negative"
# Calculer le pourcentage
percentage = (part / whole) * 100
# Postcondition - le résultat doit être un pourcentage valide
assert 0 <= percentage <= 100, f"percentage {percentage} is outside valid range"
return percentage
# Cela fonctionne correctement
percentage = calculate_percentage(25, 100)
print(f"Percentage: {percentage}%") # Output: Percentage: 25.0%
# Cela révèle une erreur de logique dans notre fonction
# (nous n’avons pas vérifié que part <= whole)
percentage = calculate_percentage(150, 100) # AssertionError: percentage 150.0 is outside valid rangeL’assertion de postcondition a détecté un bug dans notre fonction—nous avons oublié de valider que part ne dépasse pas whole. C’est exactement à cela que servent les assertions : détecter des erreurs de programmation.
26.4.4) Les assertions peuvent être désactivées
Une caractéristique importante des assertions est qu’elles peuvent être désactivées lorsqu’on exécute Python avec l’option -O (optimize) :
# Ce fichier s’appelle test_assertions.py
def divide(a, b):
assert b != 0, "divisor cannot be zero"
return a / b
result = divide(10, 2)
print(f"Result: {result}")
result = divide(10, 0) # AssertionError when assertions are enabledExécution normale :
python test_assertions.py
# Output: Result: 5.0
# Then: AssertionError: divisor cannot be zeroExécution avec optimisation :
python -O test_assertions.py
# Output: Result: 5.0
# Then: ZeroDivisionError: division by zeroC’est pourquoi les assertions ne doivent jamais être utilisées pour valider des données externes—si quelqu’un exécute votre programme avec -O, toutes les assertions sont ignorées. Utilisez les assertions uniquement pour détecter des bugs de programmation pendant le développement et les tests.