Prérequis : fonctions en Python , programmation orientée objet (pour les exceptions personnalisées)
À l’issue de ce chapitre, vous saurez :
print, assertions, débogueur (pdb/breakpoint) ;try/except/finally et lever des exceptions avec raise ;doctest, unittest ou pytest ;Un code simple, clair et lisible sera beaucoup plus facile à déboguer : choisir des noms de variables significatifs, spécifier et documenter les fonctions, privilégier un découpage en plusieurs fonctions simples plutôt que de gros blocs de codes…
Python permet d’annoter les paramètres et la valeur de retour d’une fonction avec le type attendu :
def moyenne(notes: list[float]) -> float:
return sum(notes) / len(notes)
L’interpréteur n’en tient pas compte (aucune vérification à l’exécution), mais ces annotations documentent la fonction, sont lues par l’éditeur (complétion, signalement d’un appel avec un mauvais type) et par des outils de vérification comme mypy. Elles complètent la docstring et les assertions : la docstring dit ce que fait la fonction, l’annotation sur quels types, l’assertion vérifie une propriété à l’exécution.
Les éditeurs utilisent des codes couleurs (mots réservés en couleur, …). En observant les couleurs d’un script, on peut voir que l’on n’a pas refermé une chaine de caractère, mal tapé une instruction, utiliser une fonction prédéfinie comme nom de variable (min par exemple)…
Le message d’erreur donne le type d’erreur et la ligne où l’erreur est apparue. Ce n’est pas forcément la ligne fautive. L’erreur peut venir d’une parenthèse absente à la ligne précédente, d’une erreur à tout autre endroit du code entrainant qu’une variable n’a pas le comportement attendu.
Quelques messages d’erreur :
SyntaxError: invalid syntax → Syntaxe pas correcte : mot mal orthographié, symbole
manquant, …IndexError: list index out of range → Débordement dans une liste : la liste compte par
exemple 4 élément et on essaie d’accéder au 5ème.TypeError: 'int' object is not callable → Il y a des parenthèses après une variable du
type int ; un objet « callable » est une fonction.TypeError: 'float' object is not subscriptable → Il y a des crochets après une
variable du type float ; un objet « subscriptable » est un conteneur (une liste, une
chaine de caractères…).La méthode la plus classique de débogage, toujours efficace, consiste à rajouter des affichages dans son
code, en insérant une ou plusieurs instructions print provisoires pour voir les valeurs des variables à des
points critiques.
Une assertion va interrompre l’exécution du code si la condition n’est pas vérifiée. On peut utiliser des assertions pour vérifier :
return pour s’assurer que le traitement est
conforme aux attentes, le fait qu’une fonction renvoie un résultat du bon type,
positif, non vide …Syntaxe en Python : assert condition ou assert condition, message d’erreur
Exemples :
assert type(message) == strassert longueur > largeur, "La longueur n’est pas plus grande que la largeur !"Toutes les erreurs sont en fait des exceptions qui héritent de la même classe mère Exception.
Lorsque Python lève une exception, le programme est stoppé immédiatement et un message d’erreur est affiché pour prévenir l’utilisateur de la cause du problème.
Cependant parfois, il est plus intéressant de “capturer” cette exception de manière à informer l’utilisateur du problème et lui proposer une alternative, plutôt que de stopper brutalement le programme.
Pour cela on utilisera les mot-clefs try et except :
try va débuter un bloc d’instructions à risque, susceptible de
générer une erreurexcept va intercepter l’erreur pour éviter l’arrêt brutal du programme et proposer un
traitement du problème, souvent un affichageLe modèle est celui-ci :
try:
# Code à risque
except Exception as erreur:
# Traitement de l'erreur (affichage...)
Par exemple, le programme qui calcule l’inverse d’un nombre donnerait :
try:
x = int(input("Saisir x : "))
print(f"1/x = {1 / x}")
except Exception as erreur:
print(f"Erreur : {erreur}")
Si l’on souhaite différencier le traitement des erreurs, il faut ajouter plusieurs
blocs except, typiquement un par type d’erreur.
try:
x = int(input("Saisir x : "))
print(f"1/x = {1 / x}")
except ValueError as erreur:
print(f"Erreur de saisie : {erreur}")
except ZeroDivisionError as erreur:
print(f"Impossible de diviser par zéro : {erreur}")
On peut également ajouter un bloc d’instructions qui sera exécuté quoiqu’il arrive :
erreur ou comportement normal. Pour cela on ajoutera à la fin le mot clef finally:
try:
x = int(input("Saisir x : "))
print(f"1/x = {1 / x}")
except ValueError as erreur:
print(f"Erreur de saisie : {erreur}")
except ZeroDivisionError as erreur:
print(f"Impossible de diviser par zéro : {erreur}")
finally:
print("Merci d'avoir utiliser ce calculateur")
Quand on écrit son propre module Python avec ses fonctions, ses classes, ses méthodes, on a souvent besoin de lever nous même des exceptions sans attendre l’interpréteur Python.
À l’aide du mot-clef raise, on peut lever une exception à tout moment.
x = int(input("Saisir x : "))
if x == 13:
raise ValueError("Désolé, je suis superstitieux.")
print(f"1/x = {1 / x}")
Celle-ci sera interceptée par un bloc try / except ou pas. Il est également possible de
définir ses propres exceptions personnalisées en la rattachant à la classe Exception via
l’héritage.
class SuperstitionError(Exception):
pass
x = int(input("Saisir x : "))
if x == 13:
raise SuperstitionError("Désolé, je suis superstitieux.")
print(f"1/x = {1 / x}")
L’insertion de la ligne import pdb; pdb.set_trace() (ou breakpoint() à partir de la version 3.7) dans un
code mettra l’exécution du code en pause au niveau de cette ligne et entrera dans le mode débogage.
On peut alors consulter les valeurs des variables dans l’explorateur de variables de l’éditeur ou dans la
console en tapant p suivi du nom de la variable, par exemple p n pour la valeur de la variable n.
On peut utiliser pp (pretty print) pour afficher plus lisiblement les collections d’objets.
Les commandes du débogueur sont à l’adresse https://docs.python.org/fr/3/library/pdb.html
.
Citons principalement :
n(ext) exécute la ligne en cours, et renvoie de suite le résultat d’une fonction appelées(tep) est identique à next mais va entrer à l’intérieur d’une fonction appeléer(eturn) continue l’exécution jusqu’au return de la fonction en coursc(ontinue) l’exécution jusqu’à la fin du script ou jusqu’au prochain point d’arrêt! en début de ligne permet d’entrer des instructions en Pythonh(elp) affiche l’aide du débogueurCe débogueur est intégré dans la plupart des éditeurs.
En Python, si a est la liste [1, 2, 3], l’expression a * 2 renvoie [1, 2, 3, 1, 2, 3].
Si a est le tableau numpy [1, 2, 3], l’expression a * 2 renvoie le tableau numpy [2, 4, 6].
En C, après l’exécution du code suivant, a sera égal à 3. On peut perdre la partie décimale par erreur.
int a;
float b = 3.45;
a = b;
Un dépassement de capacité lors d’une conversion de flottant en entier a causé l’explosion de la fusée Ariane 5 en 1996 !
En C, la variable b définie ci-dessous devient en pratique négative (elle vaut -1794967296) : la somme dépasse la capacité d’un int, ce que la norme du langage C laisse d’ailleurs indéfini.
int a = 2000000000;
int b = a + 500000000;
On parle d’effet de bord lorsqu’une fonction modifie un état extérieur à son environnement local. Par exemple, une fonction agissant sur une liste définie en dehors de la fonction crée un effet de bord. Il est souvent difficile de suivre les conséquences de ces modifications, ce qui est source de bugs. Les effets de bord n’existent pas en programmation fonctionnelle.
On essaie d’accéder à un élément d’un tableau (ou liste) vide, ou au dixième élément d’un tableau comptant neuf éléments. Il faut toujours penser à traiter le cas du tableau vide, et vérifier qu’on ne sort pas du tableau.
On suppose qu’on a défini une liste de nombres lst. Expliquer l’erreur dans le code Python suivant.
lst = []
i = 0
while lst[i] < 100 and i < len(lst):
i += 1
Lorsqu’on traite différents cas à l’aide d’instructions conditionnelles, il faut bien s’assurer qu’on oublie aucun cas.
Bien réfléchir au sens des inégalités, si on doit prendre des inégalités larges ou strictes, pour éviter les débordements par exemple. Attention aussi aux opérateurs booléens :
while not est_vide(lst1) and not est_vide(lst2): est équivalent à
while not (est_vide(lst1) or est_vide(lst2)):
Une boucle du type while x != y: ou while f(x) != 0: risque de ne jamais se terminer si x et y sont des
flottants. En effet, même si dans un modèle mathématiques, x devrait être égal à y au bout d’un certain
nombre d’exécution, ou si f(x) devrait s’annuler, les erreurs d’arrondis vont perturber les calculs et ces
conditions risquent de ne jamais se réaliser. On utilisera plutôt while abs(x - y) > 1e-10: par exemple.
Des noms de variables mal choisis vont induire le programmeur en erreur. Pour éviter les problèmes de casse (majuscules/minuscules), respecter les conventions de nommage : tous les noms de variables en minuscules, sauf pour les classes et les noms composés. Dans ce dernier cas, on utilise la convention camel case ou snake case.
On teste son code régulièrement, morceau par morceau, pour s’assurer que chaque fonction remplit bien son rôle ou pour comparer les temps d’exécution.
On peut tester son code « à la main », utiliser des fonctions de tests ou écrire ses scripts de tests. L’utilisation de fonctions de tests ou de scripts s’impose pour le développement d’applications dans lequel on va coder plusieurs versions d’une fonction, puis effectuer des mises à jour. On peut alors réutiliser la même base de tests après chaque modification du code ou avec chaque nouvelle version de la fonction.
Certains programmeurs pratiquent le TDD : Test-Driven Development (développement piloté par les tests). Ils écrivent d’abord les scripts de tests, puis écrivent les codes requis pour passer les tests.
L’ensemble des valeurs utilisées pour tester son code est appelé jeu de tests. Un bon jeu de tests va explorer les situations critiques, en cherchant à mettre en défaut le programme, utilisant par exemple les valeurs minimales et maximales des variables.
Pour effectuer un grand nombre de tests, on peut générer des données aléatoires, par exemple des listes
de nombres en utilisant la bibliothèque random, ou utiliser un site comme https://generatedata.com/
.
Le module Python unittest fournit un riche ensemble d’outils pour construire et lancer des tests.
https://docs.python.org/fr/3/library/unittest.html
On veut tester une fonction fact calculant \(n!=1\times 2\times 3\cdots\times n\).
Par définition, \(0! = 1\) et on souhaite que notre fonction renvoie une erreur d’assertion si n est négatif.
def fact(n):
assert n >= 0, "n est un entier naturel !"
if n == 0:
return 1
else:
return n * fact(n - 1)
Pour tester cette fonction, on écrit les lignes suivantes à la suite du script ou dans
un autre script dédié aux tests, en important la fonction fact.
import unittest
from mon_fichier import fact
class TestFact(unittest.TestCase):
def test_calcul_fact_5(self):
self.assertEqual(fact(5), 120)
def test_calcul_fact_0to6(self):
self.assertEqual([fact(n) for n in range(7)], [1, 1, 2, 6, 24, 120, 720])
def test_calcul_fact_0(self):
self.assertTrue(type(fact(0)) == int)
def test_fact_n_negatif(self):
with self.assertRaises(AssertionError):
fact(-2)
if __name__ == '__main__':
unittest.main()
On crée notre classe de tests comme sous-classe de unittest.TestCase avec class NomDeLaClasseDeTests(unittest.TestCase).
Chaque méthode de cette classe correspond à un test ou à une série de tests similaires.
Les noms des méthodes doivent commencer par le mot-clef test et être le plus explicite possible car
ils vont s’afficher dans la console en cas d’erreur. On peut utiliser les fonctions de
tests suivantes :
self.assertEqual(a, b) : teste l’égalité de a et b ;self.assertTrue(a) : teste si un booléen est égal à True ;self.assertFalse(a) : teste si un booléen est égal à False ;self.assertRaises(erreur) : vérifie que le code renvoie une erreur précise (TypeError,
AssertionError, …). Afin que l’erreur n’interrompe pas l’exécution du code, on intègre
le test dans un bloc with. On appelle ensuite unittest.main() pour exécuter les tests ainsi créés.Le module Python pytest fournit également un riche ensemble d’outils pour construire et lancer des tests.
import pytest
from mon_fichier import fact
def test_calcul_fact_5():
assert fact(5) == 120
def test_calcul_fact_0to6():
assert [fact(n) for n in range(7)] == [1, 1, 2, 6, 24, 120, 720]
def test_calcul_fact_0():
assert type(fact(0)) == int
def test_fact_n_negatif():
with pytest.raises(AssertionError):
fact(-2)
if __name__ == '__main__':
pytest.main()
On peut aussi executer le fichier ci-dessus avec pytest test_fact.py si l’on n’écrit pas les deux dernières lignes.
Le module doctest effectue les tests mentionnés dans les documentations. https://docs.python.org/3/library/doctest.html
On entre des exemples d’utilisation des fonctions dans leurs documentations, avec les résultats attendus, et doctest va effectuer ses vérifications.
def fact(n):
"""
Renvoie n! pour n entier
>>> fact(5)
120
>>> [fact(n) for n in range(7)]
[1, 1, 2, 6, 24, 120, 720]
>>> fact(-2)
Traceback (most recent call last):
...
AssertionError: n est un entier naturel !
"""
assert n >= 0, "n est un entier naturel !"
if n == 0:
return 1
else:
return n * fact(n - 1)
if __name__ == "__main__":
import doctest
doctest.testmod()
Le résultat des tests s’affiche uniquement si un test échoue.
doctest.testmod(verbose=True) forcera l’affichage même si les tests réussissent.doctest.run_docstring_examples(fonction, globals()) pour tester une fonction spécifique.Les gains d’optimisation sont souvent liés :
Le chronométrage d’une portion de code ou d’une fonction peut s’effectuer avec la
fonction perf_counter() du module time comme ci-dessous :
import time
# Fonction qu'on souhaite chronométrer
def additionner(limite):
somme = 0
for i in range(0, limite + 1):
somme += i
return somme
# Lancement et chronométrage de la fonction/
debut = time.perf_counter()
resultat = additionner(1_000_000)
fin = time.perf_counter()
# Affichage du résultat et de la durée d'exécution/
print(f"La somme jusqu'à 1_000_000 est {resultat}")
delai = fin - debut
print(f"Durée d'exécution = {delai} s")
La somme jusqu'à 1_000_000 est 500000500000
Durée d'exécution = 0.078339777 s
Le profilage d’un code donne des résultats beaucoup plus complet avec pour chaque fonction :
Le module cProfile présent dans la distribution standard de Python permet de réaliser
facilement ce profilage. Il suffit de lancer l’exécution d’un script depuis un terminal
comme ci-dessous :
$ python3 -m cProfile ./mon_fichier.py
1001004 function calls in 14.529 seconds
Ordered by: standard name
ncalls tottime percall cumtime percall filename:lineno(function)
1 0.000 0.000 14.529 14.529 mon_fichier.py:1(<module>)
1 0.001 0.001 14.529 14.529 mon_fichier.py:1(faire_quelque_chose)
1000 0.180 0.000 14.529 0.015 mon_fichier.py:5(faire_autre_chose)
1000000 14.349 0.000 14.349 0.000 mon_fichier.py:9(faire_semblant)
1 0.000 0.000 14.529 14.529 {built-in method builtins.exec}
1 0.000 0.000 0.000 0.000 {method 'disable' of '_...' objects}
On peut noter par exemple que la fonction faire_semblant() a été appelée \(1000000\) de
fois pour un temps total d’exécution de \(14,349\) s soit presque l’intégralité du temps
d’exécution du programme complet.
C’est précisément sur cette fonction qu’il faudrait focaliser les optimisations.
assert) est un contrôle que le programmeur insère pour vérifier une hypothèse pendant la mise au point ; si la condition est fausse, elle lève l'exception AssertionError. Une exception signale une erreur survenue à l'exécution ; toute exception, AssertionError comprise, arrête le programme sauf si elle est capturée avec try/except, ce qui permet au programme de réagir à l'erreur au lieu de s'arrêter brutalement.while x != y quand x et y sont des flottants ?
x devrait devenir égal à y, l'égalité exacte peut ne jamais être atteinte, provoquant une boucle infinie. On utilise plutôt while abs(x - y) > epsilon avec un seuil de tolérance.cProfile par exemple) détaille le nombre d'appels et le temps passé dans chaque fonction, ce qui permet d'identifier précisément les goulots d'étranglement à optimiser.unittest et doctest ?
doctest place les tests dans la docstring de la fonction, sous forme d'exemples d'utilisation. C'est simple et sert aussi de documentation. unittest (et pytest) permettent des tests plus structurés dans des fichiers séparés, avec des assertions variées (assertEqual, assertRaises…) et une meilleure organisation pour les projets importants.breakpoint() dans un programme Python ?
pdb). On peut alors inspecter les variables, exécuter le code pas à pas (n pour next, s pour step) et comprendre le comportement du programme.IndexError. De plus, en Python, la condition lst[i] < 100 and i < len(lst) est mal ordonnée : il faut d'abord vérifier que l'indice est valide (i < len(lst)) puis accéder à l'élément, grâce à l'évaluation paresseuse du and.try/except) permettent de gérer les erreurs à l’exécution sans interrompre brutalement le programme.doctest, unittest ou pytest) doivent couvrir les cas nominaux et les cas limites ; le TDD préconise d’écrire les tests avant le code.pdb/breakpoint) permet d’exécuter le code pas à pas et d’inspecter les variables en temps réel.cProfile) identifie les fonctions les plus coûteuses pour cibler les optimisations ; ces techniques complètent la modularité
pour produire des programmes fiables.