def divise(a, b):
"""Renvoie la division de a par b ou une erreur."""
if b == 0:
return "Erreur : division par 0"
return a / b
Pourquoi est-ce une mauvaise idée ?
def divise(a, b):
"""Renvoie la division de a par b."""
if b == 0:
return float("inf")
return a / b
Pourquoi est-ce une mauvaise idée ?
def niveau_gris(rouge, vert, bleu):
"""Renvoie un niveau de gris à partir d'une couleur RVB.
rouge, vert et bleu sont des entiers dans [0 ; 255].
"""
if rouge < 0 or rouge > 255:
return None
if vert < 0 or vert > 255:
return None
if bleu < 0 or bleu > 255:
return None
return (rouge + vert + bleu) // 3
Pourquoi est-ce une mauvaise idée ?
def divise(a, b):
"""Renvoie la division de a par b et un statut."""
if b == 0:
return 0, False
return a / b, True
resultat, succes = divise(15, 0)
if succes:
print(resultat)
def niveau_gris(rouge, vert, bleu):
"""Renvoie un niveau de gris à partir d'une couleur RVB.
rouge, vert et bleu sont des entiers dans [0 ; 255].
"""
if rouge < 0 or rouge > 255:
return 0, 1
if vert < 0 or vert > 255:
return 0, 2
if bleu < 0 or bleu > 255:
return 0, 3
return (rouge + vert + bleu) // 3, 0
gris, statut = niveau_gris(255, 0, 0)
if statut == 0:
print(f"ok : {gris}")
else:
print(f"Erreur : l'argument n°{statut} n'est pas dans [0 ; 255]")
SUCCES = 0
ERREUR_ROUGE = 1
ERREUR_VERT = 2
ERREUR_BLEU = 3
def niveau_gris(rouge, vert, bleu):
"""Renvoie un niveau de gris à partir d'une couleur RVB.
rouge, vert et bleu sont des entiers dans [0 ; 255].
"""
if rouge < 0 or rouge > 255:
return 0, ERREUR_ROUGE
if vert < 0 or vert > 255:
return 0, ERREUR_VERT
if bleu < 0 or bleu > 255:
return 0, ERREUR_BLEU
return (rouge + vert + bleu) // 3, SUCCES
SUCCES = 0
ERREUR_ROUGE = 1
ERREUR_VERT = 2
ERREUR_BLEU = 3
MESSAGES = {
SUCCES : "OK",
ERREUR_ROUGE : "Rouge à l'extérieur de l'intervalle [0 ; 255]",
ERREUR_VERT : "Vert à l'extérieur de l'intervalle [0 ; 255]",
ERREUR_BLEU : "Bleu à l'extérieur de l'intervalle [0 ; 255]"
}
def niveau_gris(rouge, vert, bleu):
# [...]
gris, statut = niveau_gris(255, 0, 0)
if statut == 0:
print(f"ok : {gris}")
else:
print(f"Erreur : {MESSAGES[statut]}")
liste = [0, 1, 2]
liste[3]
IndexError: list index out of range
resultat = 1 / 0
ZeroDivisionError: division by zero
IndexError
NameError
TypeError
ValueError
ZeroDivisionError
try:
# Bloc1 (bloc de code n°1)
except Erreur:
# Bloc2
except (AutreErreur, PasDeBol):
# Bloc3
except:
# Bloc4
else:
# Bloc5
finally:
# Bloc6
def divise(a, b):
return a / b
try:
resultat = divise(15, 0)
except ZeroDivisionError:
print("Error: division par zéro")
else:
print(f"Le résultat est {resultat}")
def divise(a, b):
return a / b
try:
resultat = divise(15, 0)
print(f"Le résultat est {resultat}")
except ZeroDivisionError:
print("Error: division par zéro")
def divise(a, b):
return a / b
try:
resultat = divise(15, 0)
print(f"Le résultat est {resultat}")
except:
print("Error: division par zéro")
def divise(a, b):
return a / b
try:
resultat = divise(15, 0)
except ZeroDivisionError:
print("Error: division par zéro")
else:
print(f"Le résultat est {resultat}")
finally:
print("On passe ici")
Error: division par zéro
On passe ici
def divise(a, b):
return a / b
try:
resultat = divise(15, 1)
except ZeroDivisionError:
print("Error: division par zéro")
else:
print(f"Le résultat est {resultat}")
finally:
print("On passe ici")
Le résultat est 15
On passe ici
except
adapté.def niveau_gris(rouge, vert, bleu):
"""Renvoie un niveau de gris à partir d'une couleur RVB.
rouge, vert et bleu sont des entiers dans [0 ; 255].
"""
if rouge < 0 or rouge > 255:
raise ValueError("Rouge en dehors de [0 ; 255]")
if vert < 0 or vert > 255:
raise ValueError("Vert en dehors de [0 ; 255]")
if bleu < 0 or bleu > 255:
raise ValueError("Bleu en dehors de [0 ; 255]")
return (rouge + vert + bleu) // 3
try:
gris = niveau_gris(255, -1, 0)
print(gris)
except ValueError as erreur:
print(erreur)
def f():
print("Entrée dans f")
raise ValueError("peu importe...")
print("Sortie de f")
def g():
print("Entrée dans g")
f()
print("Sortie de g")
def h():
print("Entrée dans h")
g()
print("Sortie de h")
try:
h()
except ValueError:
print("Fin")
Entrée dans h
Entrée dans g
Entrée dans f
Fin
try
def lire_valeur(cast, message, erreur):
valeur = input(f"{message} : ")
try:
return cast(valeur)
except ValueError:
print(f"{valeur} : {erreur}")
valeur = lire_valeur(int, "Entrer un entier", "n'est pas un entier")
print(valeur)
si on entre "chocolat"
chocolat : n'est pas un entier
None
def divise(a, b):
try:
return a / b
except ZeroDivisionError as erreur:
raise ValueError("Dénominateur nul") from erreur
divise(15, 0)
ZeroDivisionError: division by zero
The above exception was the direct cause of the following exception:
ValueError: Dénominateur nul
class RougeErreur(Exception):
pass
class VertErreur(Exception):
pass
class BleuErreur(Exception):
pass
def niveau_gris(rouge, vert, bleu):
"""Renvoie un niveau de gris à partir d'une couleur RVB.
rouge, vert et bleu sont des entiers dans [0 ; 255].
"""
if rouge < 0 or rouge > 255:
raise RougeErreur("Rouge en dehors de [0 ; 255]")
if vert < 0 or vert > 255:
raise VertErreur("Vert en dehors de [0 ; 255]")
if bleu < 0 or bleu > 255:
raise BleuErreur("Bleu en dehors de [0 ; 255]")
return (rouge + vert + bleu) // 3
try:
gris = niveau_gris(255, -1, 0)
except RougeErreur as e:
print(f"Ecarlate : {e}")
except VertErreur as e:
print(f"Trop vert : {e}")
except BleuErreur as e:
print(f"Schtroumpf : {e}")
Trop vert : Vert en dehors de [0 ; 255]
racine_carree
x
est un nombre flottant positif ou nul.epsilon
est un nombre flottant strictement positif.x
et epsilon
sont inchangés.x
, à plus ou moins epsilon
.x
et epsilon
doivent être différents de NAN (Not A Number) et de l'infinie.x
et epsilon
peuvent également être des entiers.def divise(a, b):
"""Divise a par b.
a - nombre flottant.
b - nombre flottant non nul.
Retourne la division a / b.
"""
return a / b
def divise(a, b, epsilon=0.000001):
"""Divise a par b.
a - nombre flottant.
b - nombre flottant non nul.
epsilon - valeur autour de laquelle b est considérée nulle.
Retourne la division a / b.
"""
if abs(b) < epsilon:
raise ValueError("b est trop proche de 0")
return a / b
def divise(a, b, epsilon=0.000001):
"""Divise a par b.
a - nombre flottant.
b - nombre flottant non nul.
epsilon - valeur autour de laquelle b est considérée nulle.
Retourne la division a / b si a et b sont corrects.
"""
if type(a) != float and type(a) != int:
raise TypeError("a n'est ni int, ni float")
if type(b) != float and type(b) != int:
raise TypeError("b n'est ni int, ni float")
if abs(b) < epsilon:
raise ValueError("b est trop proche de 0")
return a / b
assert
.True
, le programme continue son exécution.AssertionError
est levée.assert 3 % 2 == 0
print("3 est divisible par 2")
AssertionError
assert 3 % 2 == 0, "Si 3 était divisible par 2, on le saurait"
print("3 est divisible par 2")
AssertionError: Si 3 était divisible par 2, on le saurait
assert 3 % 2 == 1
print("Le reste de la division de 3 par 2 est 1")
Le reste de la division de 3 par 2 est 1
min
, on a 2 entiers en entrée.a < 0 | a == 0 | a > 0 | |
---|---|---|---|
b < 0 | 3 tests (a < b, a == b, a > b) | Test 6 | Test 9 |
b == 0 | Test 4 | Test 7 | Test 10 |
b > 0 | Test 5 | Test 8 | 3 tests |
def min(a, b):
"""Renvoie le minimum entre a et b.
a - entier.
b - entier.
Renvoie a s'il est plus petit que b et b sinon.
"""
if b == 424242: # bug ou backdoor
return a
return a if a < b else b
def min(a, b):
if b == 424242:
return a
return a if a < b else b
for
break
, continue
, return
, yield
, etc.while
for
.while len(L) > 0 and (L[i] == "ok" or est_vrai):
# [...]
def racine_carree(x, epsilon=0.000001):
"""Renvoie la racine carrée de x à epsilon près."""
if x < 0:
raise ValueError("x est négatif")
s = x / 2
while abs(s ** 2 - x) >= epsilon:
P = s ** 2 - x
P_prime = 2 * s
s = s - P / P_prime
return s
def test_racine_carree_25():
# Arrange
x = 25
epsilon = 0.00001
attendu = 5
# Agit
resultat = racine_carree(x, epsilon)
# Affirme
assert abs(resultat - attendu) <= epsilon
def test_racine_carree_25_grand_epsilon():
# Arrange
x = 25
epsilon = 0.1
attendu = 5
# Agit
resultat = racine_carree(x, epsilon)
# Affirme
assert abs(resultat - attendu) <= epsilon
def test_racine_carree_0():
# Arrange
x = 0
epsilon = 0.00001
attendu = 0
# Agit
resultat = racine_carree(x, epsilon)
# Affirme
assert abs(resultat - attendu) <= epsilon
def test_racine_carree_1():
# Arrange
x = 1
epsilon = 0.00001
attendu = 1
# Agit
resultat = racine_carree(x, epsilon)
# Affirme
assert abs(resultat - attendu) <= epsilon
def test_racine_carree_negatif():
# Arrange
x = -1
exception_attrappee = False
# Agit
try:
racine_carree(x)
except:
exception_attrappee = True
# Affirme
assert exception_attrappee
def racine_carree(x, epsilon=0.000001):
"""Renvoie la racine carrée de x à epsilon près."""
pass
def test_racine_carree_25():
# Arrange
x = 25
epsilon = 0.00001
attendu = 5
# Agit
resultat = racine_carree(x, epsilon)
# Affirme
assert abs(resultat - attendu) <= epsilon
def racine_carree(x, epsilon=0.000001):
"""Renvoie la racine carrée de x à epsilon près."""
return 5
def test_racine_carree_25_grand_epsilon():
# Arrange
x = 25
epsilon = 0.1
attendu = 5
# Agit
resultat = racine_carree(x, epsilon)
# Affirme
assert abs(resultat - attendu) <= epsilon
def test_racine_carree_0():
# Arrange
x = 0
epsilon = 0.00001
attendu = 0
# Agit
resultat = racine_carree(x, epsilon)
# Affirme
assert abs(resultat - attendu) <= epsilon
def racine_carree(x, epsilon=0.000001):
"""Renvoie la racine carrée de x à epsilon près."""
return 5 if x == 25 else 0
def test_racine_carree_1():
# Arrange
x = 1
epsilon = 0.00001
attendu = 1
# Agit
resultat = racine_carree(x, epsilon)
# Affirme
assert abs(resultat - attendu) <= epsilon
def racine_carree(x, epsilon=0.000001):
"""Renvoie la racine carrée de x à epsilon près."""
if x == 25:
return 5
elif x == 0:
return 0
elif x == 1:
return 1
def test_racine_carree_negatif():
# Arrange
x = -1
exception_attrappee = False
# Agit
try:
racine_carree(x)
except:
exception_attrappee = True
# Affirme
assert exception_attrappee
def racine_carree(x, epsilon=0.000001):
"""Renvoie la racine carrée de x à epsilon près."""
if x < 0:
raise ValueError("x est négatif")
elif x == 25:
return 5
elif x == 0:
return 0
elif x == 1:
return 1
Dans ce cours, on va aborder 2 points importants et complémentaires : - Comment gérer les erreurs ? - Comment vérifier que son code fonctionne comme spécifié ?
Nous allons voir différentes manières de gérer les erreurs, et revenir sur les principales philosophies de gestion des erreurs.
C'est une mauvaise idée car le type de retour change en fonction du chemin d'exécution. Cela signifie que lorsque l'on appelle cette fonction, on doit vérifier s'il s'agit d'une chaîne de caractères.
Vous avez appris en mathématiques que l'infini ne se traite pas comme un nombre. Vous avez notamment appris à manipuler les limites en "+ l'infini" et en "- l'infini". Si on renvoie sans prévenir des nombres flottants particuliers comme l'infini, ou NaN (Not a Number), on s'éloigne complètement du domaine des nombres Réels. Cela amène presque toujours à des bugs subtiles et difficiles à corriger. Il vaut mieux ne jamais renvoyer explicitement l'infini ou NaN pour représenter une erreur.
Encore une fois, le type de retour n'est pas uniforme, ce qui est généralement une mauvaise idée car c'est difficile à utiliser correctement. Par ailleurs, on n'a aucune idée de l'origine du problème : il n'y a pas de distinction faite entre les différents problèmes possible. Est-ce le rouge qui ne va pas ? Est-ce le bleu ?
Dans ce cas, on conserve toujours le même type de retour, ce qui est une bonne chose. Ici, il n'y a qu'une seule erreur possible, donc on peut se contenter de renvoyer un Booléen qui indique si l'opération est un succès.
Dans cette solution, on conserve toujours le même type de retour. On est également capable de distinguer les différents types d'erreur. En revanche, cette solution n'est pas parfaite : - Les types de retour "retour 0, 1" sont difficiles à lire. - On n'a pas de message interne pour informer de l'erreur : l'interprétation se fait à l'extérieur.
Il reste des problèmes : - Les constantes n'existent pas vraiment en Python. Donc il serait techniquement possible de réassigner SUCCES. - Il n'y a toujours pas de message associé aux erreurs.
Nous allons voir un autre mécanisme plus adapté à la gestion des erreurs en Python. Ce mécanisme n'existe pas dans tous les langages de programmation (par exemple, le langage de programmation C). Par conséquent, l'approche vue dans ces dernières diapositives peut continuer à être utilisée avec ces langages.
Dans certains langages de programmation, comme le C++, les exceptions sont réservées aux cas exceptionnels. En Python, les exceptions ne sont pas rares et on les trouve partout pour gérer les erreurs. Ici, IndexError est un type d'exception levée par Python alors que l'on tente d'accéder à un élément en-dehors de l'intervalle.
Ces exceptions surviennent lorsque le programme tente d'exécuter une expression dont la sémantique est erronée. Dans de nombreux cas, il s'agit d'un bug dans le programme, et non d'une erreur qu'il faut gérer.
Si on demande un nombre strictement positif et que l'utilisateur rentre 0, que doit faire le programme ? Il est nécessaire de spécifier puis d'implémenter le comportement approprié au cas d'usage.
On simplifie notre code puisque la division par zéro est déjà détectée. La gestion d'erreur se fait ici de manière naturelle.
Ce code a exactement le même comportement que celui sur la diapositive précédente. En effet, dès qu'une exception est levée, on sort immédiatement du bloc de code actuel pour aller dans le bloc de gestion de l'exception. La différence est essentiellement stylistique : - Le code sur cette diapositive sera mieux compris par des développeurs non habitués au Python, car cette syntaxe existe dans de nombreux langages de programmation. - Le code sur la diapositive précédente plaira plus à des experts Python, car on rend plus visible ce qui peut émettre une exception. Le choix se fera donc en fonction du contexte : si on est dans une équipe d'experts Python, on choisira plutôt la solution précédente.
En règle général, il vaut mieux être aussi spécifique que possible. En effet, dans une base de code industrielle, les occasions d'attraper une exception sont nombreuses. Si on attrape toutes les exceptions, il est possible que l'on en attrape trop. NB: le lapin fait référence à Pikachu des Pokemon dont le motto est : attrapez les tous... Ce lapin est naïf car il veut attraper toutes les exceptions d'un coup.
On a maintenant une solution élégante et performante pour gérer les différentes erreurs.
Cette fonction lit une valeur depuis l'entrée, puis tente de la caster en utilisant une fonction en entrée. Si le cast échoue, il va lever une exception de type ValueError. Dans ce cas, plutôt que d'exécuter le "return", le flot d'exécution passe dans le except. Dans ce cas, il n'y a pas de "return" exécuté. En Python, une fonction qui ne retourne rien retourne None. Donc en pratique, c'est None qui est retourné implicitement si une exception est levée.
Lorsqu'on lève une nouvelle exception dans le contexte d'une exception précédente, c'est une bonne pratique de les chaîner afin que le contexte soit conservé. On fait cela grâce à la syntaxe "from" au niveau de la levée de la nouvelle exception. Cela permet d'aider au débogage lorsque des problèmes surviennent. Loi de Murphy : des problèmes **vont** survenir !
En utilisant rigoureusement cette syntaxe, vous définissez vos propres exceptions. En pratique, cette syntaxe signifie que l'on défini une nouvelle classe qui hérite de la classe Exception. On hérite de toutes les propriétés de cette classe, et on en surcharge aucune, et on n'en défini par de nouvelle.
Définir ses propres exceptions est une bonne pratique lorsque l'on implémente une bibliothèque. Par exemple, si on implémente une bibliothèque pour gérer des requêtes HTTP, on aura sans doute envie d'avoir une exception HTTPError. Cela permet de distinguer les exceptions levées par votre bibliothèques des autres types d'exception. C'est une généralisation du cas où l'on expliquait que ce n'est pas idéal d'attraper toutes les exceptions en même temps.
Aucune vérification n'est effectuée, mais les préconditions sont bien spécifiées : il s'agit donc de l'approche offensive.
On évite le cas de la division par zéro en effectuant une vérification préalable : il s'agit de l'approche défensive.
En plus d'empêcher la division par zéro, les types des entrées sont vérifiés dynamiquement. Cette approche est rarement souhaitable car les performances d'exécution peuvent être impactées négativement. Par ailleurs, le code devient plus difficile à lire et à comprendre.
Il est possible de rajouter un message d'erreur
Si l'expression est Vraie, le flot de contrôle continue.
Les prochaines parties de ce cours vont introduire le concept de tests unitaires.
Ce partitionnement en 13 tests devrait permettre de découvrir de nombreux bugs dans la fonction min. Mais pas forcément tous les bugs.
A moins de tester explicitement la valeur 424242, le partitionnement précédent a très peu de chances de découvrir le bug dans cette implémentation. NB : une backdoor est un bug mis à dessein pour permettre à un groupe d'exploiter ce bug à leur profit.
Ces équipes peuvent également porter le nom d'équipe QA (pour Quality Assurance) ou VV (pour Vérification et Validation).
Par exemple, un programme qui doit gérer une valeur qui ne doit pas être égale à 0. Le développeur pourrait ne pas gérer du tout ce cas, et ne pas le tester. Ainsi, le bug en 0 ne serait jamais vu par le développeur (avant qu'un client ne s'en aperçoive).
Le développeur doit respecter des délais de livraison et doit aller vite. Le testeur est souvent tenu pour responsable si des bugs sont trouvés en production. Cette concurrence bénéficie à la fois aux délais de livraison et à la qualité logicielle.
Ce processus est largement manuel. Il consomme donc un temps très important.
La pyramide des tests présente les différents types de tests automatiques et leur positionnement. Les plus importants, qui composent la base, sont les tests unitaires. A chaque niveau, on trouve à gauche le pourcentage de couverture de code idéal. A droite sont listés quelques entités testés à ce niveau. Les tests de composants se font au niveau des interfaces des composants logiciels. Les tests d'intégration automatisent des tâches avec l'interface utilisateur, la base de données, les APIs web, etc. Les tests exploratoires se font manuellement. Les tests manuels ne devraient pas représenter plus de 5% de l'effort de test.
Lorsque l'on teste du code qui dépend du réseau ou d'une base de donnée ou d'un composant tier, on créé un Mock. Un Mock est une fonction ou une classe qui a la même interface que le composant à remplacer. Un Mock suit la philosophie du "Duck Typing" : si cela ressemble à un canard, c'est un canard et on peut l'utiliser là où on attend des canards. Nous ne rentrerons pas dans les détails dans ce cours. Retenez simplement que des outils existent pour conserver l'indépendance des tests unitaires.
Dans beaucoup d'entreprises, les tests automatiques sont malheureusement délaissés. Le TDD est une méthodologie qui permet d'éviter de finir avec une base de code complexe et sans tests automatiques.
Cela fonction toujours !
Cela ne calcule pas vraiment une racine carrée, mais cela passe les tests unitaires ! Cela montre aussi les limites des tests unitaires. Il existe bien d'autres méthodologies : - Acceptance Tests, - Test Fuzzing, - Etc. Cela sort du cadre de ce cours.