Prérequis : cours sur la mise au point des programmes
À l’issue de ce TP, vous saurez :
- utiliser les exceptions pour rendre un programme robuste face aux saisies incorrectes ;
- identifier et corriger des bogues dans un programme existant ;
- écrire des tests unitaires avec
assertet avecpytest; - mesurer et comparer les performances d’un programme à l’aide de
time.perf_counter().
Introduction
Les activités pratiques qui suivent ont pour objectif de vous faire appliquer les notions présentées dans le cours :
- les exceptions ;
- les bogues ;
- les tests unitaires ;
- les performances.
PizzaError
Une pizzeria souhaiterait créer une sorte de drive automatique pour ses clients afin qu’ils puissent commander en ligne une pizza selon les stocks disponibles.
Un stagiaire a déjà commencé l’écriture d’un programme prototype : pizza_drive.py .
Cependant comme vous allez pouvoir le constater, le programme est largement perfectible :
- mauvaise gestion des saisies (des lettres au lieu d’un nombre) ;
- commande de pizza malgré les stocks vides ;
- plantage lorsque l’on commande la pizza 3.
À l’aide des exceptions, rendez ce programme plus robuste et plus ergonomique pour le client, c’est-à-dire sans plantage inopiné.
Indice
Provoquer chacun des trois problèmes et lire le nom de l’exception affichée : des lettres au lieu d’un nombre déclenchent ValueError, la pizza 3 IndexError. Une boucle while True peut alors redemander le choix tant qu’il n’est pas valide : try ... except ValueError autour de la conversion, puis un test sur le numéro et un autre sur le stock. Le squelette ci-dessous remplace la boucle while de la méthode prendre_la_commande.
Code à copier puis compléter
while True:
try:
self.choix_client = int(input("Votre choix ? "))
except ValueError:
print("Veuillez saisir un nombre.")
continue
if self.choix_client not in (0, 1, 2):
print("Ce numéro n'existe pas.")
elif ...:
print("Cette pizza n'est plus disponible.")
else:
break
Correction publiée après la séance.
Chasse aux bogues
Téléchargez et décompressez l’archive bogues.zip .
Pour chacun des scripts :
- trouvez l’origine du bogue ;
- proposez un correctif pour le rendre fonctionnel.
Indice
Exécuter le script et comparer le résultat obtenu avec le résultat attendu. En cas d’erreur, lire le message en partant de la dernière ligne : le type de l’erreur, puis la ligne en cause. Sinon, afficher les valeurs des variables avec
print, ou suivre l’exécution pas à pas dans le débogueur de Thonny.
Correction publiée après la séance.
Tests unitaires
À votre tour de mettre en œuvre les tests unitaires sur une classe Rectangle à réaliser. Vous utiliserez la méthode simple avec le mot-clef assert, puis la bibliothèque pytest dans un second temps.
Classe Rectangle
L’entreprise GEOFORME développe des logiciels et outils de dessin industriel.
Le développeur avant vous dans le service a commencé à développer une classe Rectangle
disponible dans le fichier rectangle.py
. Il a principalement défini l’API de la classe
et positionné des TODO comme ci-dessous dans chaque méthode :
# TODO : compléter le code de la méthode
pass
Si vous détectez des problèmes potentiels, vous pourrez les signaler en levant une
Exception comme ci-dessous :
# Un problème potentiel...
if probleme == True:
raise Exception("Un problème est survenu")
Le responsable du service vous demande de finir de coder cette classe et d’écrire les tests unitaires qui permettront de la valider et de l’intégrer dans le reste du logiciel de dessin industriel.
Concernant les tests unitaires, vous créerez deux versions et donc deux fichiers :
tests_rectangle_assert.py: version avec le mot-clefasserttests_rectangle_pytest.py: version avec la bibliothèquepytest
Indice
Compléter d’abord le constructeur et les accesseurs, puis chaque méthode de calcul, en écrivant ses tests au fur et à mesure sur des rectangles dont on connaît les résultats de tête. Pour les problèmes potentiels, se demander quelles dimensions n’ont pas de sens pour un rectangle. Avec pytest, chaque test est une fonction dont le nom commence par test_, et la commande python -m pytest tests_rectangle_pytest.py les exécute toutes.
Code à copier puis compléter
import pytest
from rectangle import Rectangle
def test_aire():
assert Rectangle(5, 3).calculer_aire() == ...
def test_dimension_negative():
with pytest.raises(Exception):
...
Correction publiée après la séance.
Optimisation et performances
Chasse aux performances
Johnny Begood participe en ce moment à un challenge algorithmique sur une plate-forme en ligne. Pour le problème n°10, il doit écrire un programme qui affiche le \(2000\text{-ième}\) nombre premier. Il a écrit un code qui fonctionne parfaitement bien (le résultat est \(17~389\)), mais la plate-forme a fixé une limite de temps d’exécution (< 100 ms) pour valider le challenge et son programme est trop lent.
Chronométrage
Téléchargez son code source : probleme_v00.py .
À l’aide de la fonction
perf_counter()du moduletime, mesurez et affichez le temps d’exécution de son programme et plus particulièrement de sa fonctionrechercher_nieme_nombre_premier().Indice
time.perf_counter()renvoie un instant en secondes : le relever juste avant et juste après l’appel de la fonction, puis afficher la différence, comme dans l’exemple du cours.Notez le résultat moyen obtenu sur trois exécutions et confirmez le refus par la plate-forme.
Première optimisation idiomatique
Johnny a entendu dire qu’en Python les boucles for étaient plus performantes que les
boucles while.
Créez un nouveau script
probleme_v02.py, copiez-collez le code de la versionv00et modifiez le code de sa fonctionetre_premier()pour remplacer la bouclewhilepar une bouclefor.Indice
La boucle
whilefait prendre àfacteurles valeurs 2, 3, …, n − 1 : quelrangeproduit ces mêmes valeurs ? Les lignesfacteur = 2etfacteur += 1deviennent alors inutiles.Chronométrez le temps d’exécution obtenu et notez le résultat moyen sur trois lancements. Comparez le temps entre les versions
v00etv02. Est-ce suffisant ?
Seconde optimisation algorithmique
Après avoir lu la page Wikipédia consacrée aux nombres premiers, il découvre qu’un nombre N non premier possède toujours un diviseur compris entre 2 et \(\sqrt{N}\) : il suffit donc de tester les facteurs jusqu’à \(\sqrt{N}\).
Créez un nouveau script
probleme_v03.pyà partir de la précédente version et modifiez la limite de la boucleforde \(n-1\) à \(\sqrt{n}\).Indice
rangen’accepte que des entiers : la borne s’écritint(n ** 0.5) + 1, le+ 1permettant de tester aussi la racine elle-même, comme pour 25 = 5 × 5.Chronométrez cette nouvelle version comme précédemment et concluez.
Correction publiée après la séance.
Profilage
Effectuez un profilage du script de départ probleme_v00.py et notez les
résultats :
- pour la fonction
rechercher_nieme_nombre_premier():- nombre d’appels ;
- temps moyen d’exécution ;
- temps total d’exécution.
- pour la fonction
etre_premier():- nombre d’appels ;
- temps moyen d’exécution ;
- temps total d’exécution.
Indice
Lancer python -m cProfile probleme_v00.py dans un terminal, depuis le dossier du script. Dans le tableau affiché, ncalls donne le nombre d’appels, percall le temps moyen d’un appel, et cumtime le temps total, y compris celui des fonctions appelées.
Correction publiée après la séance.