Session pratique
Durée2h30Présentation & objectifs
Dans cette session, vous allez concevoir un système robuste d’analyse de données issues d’un capteur de température. Votre application ne doit jamais s’arrêter sur une erreur, même en cas :
- de données corrompues,
- de valeurs aberrantes,
- de panne du capteur. Il vous faudra donc gérer les situations imprévues et les erreurs d’exécution du programme pour assurer la continuité du service.
De plus, votre application doit pouvoir être facilement étendue avec d’autres fonctionnalités. Pour répondre à ces exigences vous veillerez à :
- appliquer les principes de la programmation défensive,
- tester les fonctionnalités de votre code,
- documenter et commenter votre code pour faciliter son utilisation et sa complétion,
- respecter les conventions de codage pour partager du code de qualité.
Le but de cette session est de vous aider à maîtriser des notions importantes en informatique. Un assistant de programmation intelligent tel que GitHub Copilot, que vous avez peut-être déjà installé, sera capable de vous fournir une solution à ces exercices basée uniquement sur un nom de fichier judicieusement choisi.
Dans un but d’entraînement, nous vous conseillons de désactiver ces outils en premier.
À la fin de l’activité pratique, nous suggérons que vous travailliez sur l’exercice à nouveau avec ces outils activés. Suivre ces deux étapes améliorera vos compétences à la fois fondamentalement et pratiquement.
Organisation et préparation de l’activité pratique
Vous allez réaliser cette activité à deux (et maximum un trinôme dans le groupe si nécessaire). Les développeurs étant désignés dans le reste du document en tant qu’étudiant1 et étudiant2. L’activité est découpée en deux parties donnant lieu à deux programmes ; un capteur et un lecteur/analyseur de données.
Environnement collaboratif
À deux, commencez par faire un fork
du dépôt prog_session2_sensor en tant qu’étudiant1
puis ajoutez étudiant2
a également accès en tant que developer
.
En cas de travail en trinôme, deux étudiants travaillent sur le même programme mais sur deux branches différentes du projet.
Le dépôt cloné a la structure de fichiers suivante :
readme.md
à compléter avec une description au format markdown du programme et de la composition de l’équipe de développeurs,- un répertoire
Src
avec deux sous-répertoiresSensor
etReader
. Ces répertoires contiennent des fichiers de tests unitaires nommés respectivementtests_sensor.py
,tests_reader.py
ettests_analyzer.py
, - un répertoire
Doc
avec deux sous-répertoiresSensor
etReader
.
Versionnez fréquemment votre code avec des messages de commit
explicites.
Aides techniques en Python
Lecture/écriture dans un fichier
Pour manipuler un fichier de votre système depuis votre code Python, il faut disposer d’un lien vers le fichier, lien que l’on appelle un descripteur et de type TextIOWrapper
.
L’obtention d’un descripteur s’effectue à l’aide de la méthode open
et lorsque le fichier n’est plus utilisé le descripteur doit être libéré en fermant le fichier avec la méthode close
, ce qui permet également de s’assurer que toutes les données ont bien été écrites sur le fichier avant la fin du programme.
file_d = open("data.txt", 'r')
# use the file
file_d.close()
Il est cependant conseillé, pour ne pas oublier de fermer le lien ouvert vers le fichier, d’encapsuler l’utilisation du fichier par un bloc d’instruction with
comme illustré ci-dessous :
with open("data.txt", 'r') as file:
# use the file
# no need to close
À la fin du bloc with
le descripteur file
est automatiquement fermé.
L’argument r
passé à la méthode open
indique que le fichier est ouvert en mode lecture.
Il peut être remplacé par w
pour ouvrir le fichier en mode écriture, a
pour ouvrir le fichier en mode ajout.
Bien que cette solution ne soit pas optimale, vous allez pour le moment ouvrir un lien vers le fichier à chaque tentative d’écriture ou de lecture.
Temporisation
L’écriture des températures relevées par le programme sensor dans le fichier data.txt
ainsi que la lecture de ces données par le programme reader s’effectuent selon une certaine fréquence en secondes. Pour mettre en pause un programme x secondes, vous pouvez utiliser la fonction sleep
du module time
.
time.sleep(.7) #mettre le programme en pause 0,7 secondes
Pseudo-aléatoire
Vous aurez besoin de simuler de l’aléatoire à l’aide du module random
. Voici ci-dessous des exemples pratiques pour retourner pseudo-aléatoirement une valeur de la liste passée en paramètre ou une valeur suivant une distribution gaussienne :
import random
l = ["ERR1", "ERR2", "ERR3"]
# returns True or False
if random.choice([True, False]):
# returns an indice between 0 and 2 included
rand_n=random.choice(range(len(l)))
print(f"Random between 0 and {len(l)-1}: {rand_n}")
print(f"Random element from list: {l[rand_n]}")
# alternative way to get a random element
# rand_n=random.choice(l)
# print(f"Random element from list (alternative): {rand_n}")
else:
mean=10
std=3
# returns a float following the Gaussian distribution set with mean and std
rand_f=random.gauss(mean,std)
print(f"Random wrt. Gaussian distribution (mean={mean}, std={std}): {rand_f}")
Gestion des logs
Le programme analyzer
va générer des logs avec les statistiques calculées. Un moyen de générer ces messages est d’utiliser le module logging
de python. Le code ci-dessous montre comment générer des logs sur la sortie standard :
import logging
# creation and configuration of the logger
logging.basicConfig(level=logging.DEBUG) # Publish logs from the DEBUG level
logger = logging.getLogger(__name__) # get a logging instance
maxv=102.3
# use the logger to log the max value
logger.info(f"Max value recorded: {maxv}")
Lorsqu’un niveau de log est défini, tous les messages de log de ce niveau et de niveaux supérieurs (ie, plus sévères) seront publiés.
Il existe plusieurs niveaux de log, chacun ayant un degré de sévérité différent. Les niveaux de log standard sont, par ordre croissant de sévérité : NOTSET
, DEBUG
, INFO
, WARNING
, ERROR
et CRITICAL
.
Partie 1 : Un capteur et un lecteur
La première partie du projet est assez simple comme l’illustre le schéma ci-dessous. Il s’agit d’implémenter un programme sensor qui génère des mesures de température (d’un moteur à explosion par exemple), les mesures étant écrites dans un fichier nommé data.txt
. En parallèle, un programme reader lit les données du fichier data.txt
.
Chaque partie est décrite par un cahier des charges fonctionnelles associé à un ensemble de tests automatiques que vous allez utiliser pour vérifier le bon fonctionnement de votre programme.
Au cours de cette première partie (environ 45 minutes), l'étudiant1
travaille sur le programme sensor et l'étudiant2
sur le programme lecteur
Les deux programmes doivent répondre à certaines exigences :
- valider les tests unitaires fournis,
- disposer d’une documentation en anglais générée par l’outil
pydoc
à partir des docstrings présentes dans le code, - contenir du code satisfaisant au maximum la PEP8 et comportant des commentaires utiles.
Programme sensor (étudiant1
)
Le développement du programme sensor s’effectue par étape.
Étape 1 : La classe Sensor
et ses propriétés
Implémentez une classe nommée Sensor
(dans un fichier de chemin Src/Sensor/sensor.py
).
Toute instance de cette classe possède les propriétés suivantes :
_ERRORS = ["NR","OoB"]
__id
__mean
__std
__frequency
__storage_file_name
Le constructeur de la classe permet de spécifier les valeurs de chacune de ces propriétés, sauf __ERRORS
qui est fixe, c’est une propriété de classe et non d’instance) ou de leur affecter les valeurs par défaut suivantes:
__id = "sensor
__mean = 90
__std = 5
__frequency = 0.1
__storage_file_name = "../data.txt"
Chaque propriété doit être accessible via une méthode de type accesseur (getter
) dont les prototypes seront les suivants :
def get_error_types(self) -> list[str]:
def get_id(self) -> str:
def get_mean(self) -> float:
def get_std(self) -> float:
def get_frequency(self) -> float:
def get_storage_file_name(self) -> str:
Une fois le constructeur et les accesseurs définis, exécutez les tests afférents avec les commandes suivantes (à exécuter depuis le répertoire Src/Sensor
) :
python tests_sensor.py TestSensor.test_encapsulation_default_values
python tests_sensor.py TestSensor.test_encapsulation_custom_values
Étape 2 : Fonctionnalités du capteur
Vous allez désormais implémenter les fonctionnalités du capteur qui sont au nombre de deux.
-
Génération d’une température selon la loi gaussienne paramétrée par les propriétés
__mean
et__std
. Méthode de prototype :def get_temperature(self) -> float:
La température générée par la méthode
get_temperature(self)
doit retourner une valeur comprise entre 0 et 150 ou une exception de typeOutofBoundsException
(classe d’exception à créer). Une captation sur vingt en moyenne, une panne est simulée et le capteur retourne une exception de typeNotRespondingException
(classe d’exception à créer). Cette méthode doit être testée à l’aide du test suivant :python tests_sensor.py TestSensor.test_get_temperature
-
Mise en place du capteur. La méthode
run
, dont le prototype est donné ci-dessous, contient une boucle infinie.def run(self) -> None: # do something
Dans cette boucle, ouvrez un descripteur en écriture par ajout vers le fichier de données, puis interrogez le capteur. Une écriture est produite qui est soit une température, soit
OoB
en cas d’exceptionOutofBoundsException
, soitNR
en cas d’exceptionNotRespondingException
. En outre, trois types d’exception doivent être gérées par cette méthode,KeyboardInterrupt
pour arrêter l’exécution avec unCtrl-C
par exemple,IOError
en cas d’impossibilité d’ouvrir ou d’écrire dans le fichier de données etException
pour tout autre exception. Veillez à ouvrir un nouveau descripteur vers le fichier à chaque tour de boucle. Ce n’est pas idéal mais simple. Si le temps le permet, vous verrez en complément comment faire communiquer des programmes de manière plus habile.
Une fois la classe implémentée, vous pouvez exécuter tous les tests unitaires avec la commande suivante (ou directement depuis VSCode) :
python tests_sensor.py
Étape 3 : Vers un code plus exploitable
Avant de partager votre code, effectuez les vérifications suivantes :
- votre code respecte au maximum les conventions de codage de la PEP8, pour se faire, appliquer la commande suivante et tentez de vous rapprocher d’un score de 10/10 :
pylint sensor.py
- ajoutez des commentaires à votre code pour rendre son déroulement plus explicite
- ajoutez des documentations (sous forme de docstring) au module, aux classes et à leurs méthodes. Puis, générez une documentation à l’aide de la commande suivante :
python -m sensor
et déplacez la documentation dans le répertoireDoc/Sensor/
.
Une fois votre code complété, archivez une nouvelle version sur le dépôt gitlab
.
Passez à la partie 2.
Programme reader (étudiant2
)
Le développement du programme reader s’effectue par étape avec le développement de deux classes.
Étape 1 : La classe Analyzer
et ses propriétés
Implémentez une classe nommée Analyzer
(dans un fichier de chemin Src/Reader/analyzer.py
).
Toute instance de cette classe possède les propriétés suivantes :
__values
__errors
__frequency
__last_analysis
__logger
__log_file = None
Le constructeur de la classe permet de spécifier la fréquence de déclenchement des analyses (__frequency
avec comme valeur par défaut 10) et le nom du fichier recevant les logs (__log_file
initialisé par défaut à analyzer.log
). La configuration du gestionnaire de logs ainsi l’initialisation des autres propriétés sont réalisées comme suit :
self.__values = []
self.__errors = []
self.__last_analysis = 0
self.__logger = logging.getLogger(__name__) #get a logging instance
self.__logger.setLevel(logging.DEBUG) #set the logging level
self.__logger.addHandler(logging.FileHandler(self.__log_file)) #add a file handler to the logger
Certaines propriétés doivent être accessibles via une méthode de type accesseur (getter
) dont les prototypes seront les suivants :
def get_values(self) -> list:
def get_errors(self) -> list:
def get_frequency(self) -> int:
def get_logger(self) -> logging.Logger:
Les autres propriétés n’ont pas raison d’être accessibles depuis l’extérieur de la classe. Dans un autre langage de programmation, on les aurait déclarées comme privées.
Une fois le constructeur et les accesseurs définis, exécutez les tests afférents avec les commandes suivantes (à exécuter depuis le répertoire Src/Reader
) :
python tests_analyzer.py TestAnalyzer.test_encapsulation_default_values
python tests_analyzer.py TestAnalyzer.test_encapsulation_custom_values
Étape 2 : Fonctionnalités de la classe Analyzer
Vous allez désormais implémenter les fonctionnalités d’analyse (très simple) de valeurs. Il y en a que deux :
- ajout d’une données dans la base de stockage. Si c’est une valeur pouvant être convertie en réel, elle est ajoutée dans la liste
__values
sinon dans la liste des erreurs. Cette méthode déclenche également l’analyse (voir méthode suivante) dès queself.__frequency
nouvelles valeurs ont été enregistrées. Le prototype de cette méthode est le suivant :def add_data(self, value: str):
- génère le résultat de l’analyse à savoir la moyenne et les minimale et maximale des
self.__frequency
dernières valeurs présentes dans la liste__values
.def log_analysis(self):
Une fois la classe implémentée, vous pouvez exécuter tous les tests unitaires avec la commande suivante ou directement depuis VSCode:
python tests_analyzer.py
Étape 3 : Vers un code plus exploitable
Avant de partager votre code, effectuez les vérifications suivantes :
- votre code respecte au maximum les conventions de codage de la PEP8, pour se faire, appliquer la commande suivante et tentez de vous rapprocher d’un score de 10/10 :
pylint analyzer.py
- ajoutez des commentaires à votre code pour rendre son déroulement plus explicite
- ajoutez des documentations (sous forme de docstring) au module, la classe et ses méthodes. Puis, générez une documentation à l’aide de la commande suivante :
python -m analyzer
et déplacez la documentation dans le bon répertoire.
Étape 4 : La classe Reader
et ses propriétés
Implémentez une classe nommée Reader
(dans un fichier de chemin Src/Reader/reader.py
).
Toute instance de cette classe possède les propriétés suivantes :
__analyzer
__frequency
__file_name
Le constructeur de la classe permet de spécifier l’instance d’analyseur associé, la fréquence de lecture (__frequency
avec comme valeur par défaut 1.0) et le chemin du fichier dans lequel lire (__file_name
avec comme valeur par défaut ../data.txt
).
Une fois le constructeur et les accesseurs définis, exécutez les tests afférents avec les commandes suivantes (à exécuter depuis le répertoire Src/Reader
) :
python tests_reader.py TestReader.test_encapsulation_default_values
python tests_reader.py TestReader.test_encapsulation_custom_values
Étape 5 : Fonctionnalités de la classe Reader
Vous allez désormais implémenter les deux fonctionnalités de la classe Reader
:
- une méthode prenant en paramètre une ligne lue depuis le fichier. Les lignes commençant par un
#
sont ignorées, les autres sont envoyées à son analyseur. Le prototype de cette fonction est le suivant :def process_line(self, line: str)
- une méthode principale qui met en place la lecture continue sur le fichier de données. Après l’ouverture du fichier de données en lecture, la méthode entre dans une boucle infinie et tente de lire une ligne. S’il y en a une, elle la traite avec la méthode précédente, sinon le programme est mis en pause pendant une durée de
self.__frequency
secondes. Le prototype de cette fonction est le suivant :Plusieurs types d’exception doivent être gérés séparément dans cette méthodedef run(self):
run
:KeyboardInterrupt
pour arrêter avec unCtrl-C
par exemple,FileNotFoundError
lorsque le fichier de données n’existe pas,IOError
pour une erreur de lecture du fichier,Exception
pour tout autre raison d’échec. Le premier type d’exception entraîne la fin du programme. Pour les autres, un message d’erreur est affiché, puis la méthoderun()
est exécutée de nouveau après un temps de pause correspondant à deux fois la fréquence d’analyse.
Une fois la classe implémentée, vous pouvez exécuter tous les tests unitaires avec la commande suivante ou bien directement depuis VSCode :
python tests_reader.py
Étape 6 : Vers un code plus exploitable
Avant de partager votre code, effectuez les vérifications suivantes :
- votre code respecte au maximum les conventions de codage de la PEP8, pour se faire, appliquer la commande suivante et tentez de vous rapprocher d’un score de 10/10 :
pylint reader.py
- ajoutez des commentaires à votre code pour rendre son déroulement plus explicite
- ajoutez des documentations (sous forme de docstring) au module, la classe et ses méthodes. Puis, générez une documentation à l’aide de la commande suivante :
python -m reader
et déplacez la documentation dans le bon répertoire.
Evaluation par vos pairs
Lors de la partie 1, les programmes sensor
et reader
ont été développés en parallèle. Chaque étudiant récupère en local la dernière version du code poussé sur le dépôt.
Chaque étudiant va exécuter le code du capteur sensor.py
et le code du lecteur reader.py
dans un terminal dédié. Logiquement, vous devez pouvoir exécuter ces deux programmes dans n’importe quel ordre. Le capteur doit pouvoir être arrêté et ré-exécuté sans que le lecteur ne s’arrête.
Si vous n’arrivez pas à atteindre ce niveau de fonctionnalité, analysez à deux le code pour effectuer les corrections nécessaires. Profitez-en pour discuter avec votre collaborateur de ses choix en termes de :
- programmation défensive (Toutes les erreurs d’exécution possible ont-elles été gérées ? La gestion de ces erreurs d’exécution est-elle conforme au cahier des charges ? etc.),
- commentaires et documentation (Les commentaires et la documentation sont-ils d’un niveau de détail suffisant pour comprendre le code sans avoir à l’analyser ?)
- qualité du code (les conventions de codage ont-elles été respectées ?).
Partie 2 : Plusieurs capteurs et un analyseur
L’objectif de cette seconde partie est double :
- continuer à faire un retour à votre collaborateur sur la qualité de son livrable,
- compléter les fonctionnalités de l’application en gérant plusieurs types de capteurs et un système d’analyse par capteur.
Pour cette seconde partie, vous allez inverser les rôles. Étudiant2
va compléter le programme 1 sur les capteurs et Étudiant1
va compléter les analyseurs.
Lors de ce travail d’implémentation, discutez avec votre collaborateur :
- les éventuels manques ou imprécisions dans la documentation et les commentaires,
- les choix d’implémentation ou de nommage que vous désapprouvez.
Vous pouvez invoquer
pylint
sur le code de votre collaborateur pour identifier puis discuter des violations de la PEP8.
Complétion du programme sensor (étudiant2
)
Définissez un second type de capteur, dont l’objectif est d’effectuer un test de bon fonctionnement. Ce capteur retourne un booléen au lieu d’un réel.
Un capteur a donc une propriété supplémentaire qui indique son type : binary
ou continue
.
Étant donné que plusieurs capteurs écriront dans un même fichier, chaque écriture sera préfixée par le nom du capteur et son type. Considérons deux capteurs nommés sensor1
de type binaire et sensor2
de type continu, on pourra alors trouver dans le fichier de données les captations suivantes :
sensor1:BinarySensor:1
sensor1:BinarySensor:1
sensor2:ContinuousSensor:12.255
sensor1:BinarySensor:0
sensor2:ContinuousSensor:90.024
...
Commentez, documentez et validez la conformité de votre code vis-à-vis de la PEP8. Les tests unitaires de la partie 1 ne sont cependant pas adaptés à cette nouvelle version du code, il est donc inutile de tester votre code. Mais patience, dès la session prochaine, vous apprendrez à concevoir des tests unitaires. N’oubliez pas qu’un objectif de cette partie est de faire un retour à votre collaborateur, qui a développé la partie, sur la qualité de son livrable.
Complétion du programme reader (étudiant1
)
Pour le moment, le programme contient une instance de la classe Reader
et une instance de la classe Analyzer
.
Vous allez désormais modifier la classe Reader
pour qu’elle soit associée à plusieurs analyseurs.
Vous allez mettre en place deux nouveaux types d’analyseurs :
- un analyseur de captations binaires dont l’analyse vise à quantifier le pourcentage de tests réussis,
- un analyseur d’erreurs qui calcule le nombre et le pourcentage d’erreurs de chaque type par capteur. L’analyse des erreurs n’est pas déclenchée selon une certaine fréquence mais effectuée lorsque le programme
reader
est arrêté.
Un analyseur doit désormais être associé à chaque capteur pour lequel une donnée est lue dans le fichier. Les analyseurs de valeurs (binaires ou continues) dirigent leurs analyses vers un même fichier, et l’analyseur d’erreur vers un fichier de log spécifique.
Mise en commun (étudiant1
et étudiant2
)
Après avoir mis-à-jour le dépôt git sur le serveur gitlab, analysez la documentation et les messages de commit
qui concernent les modifications effectuées par votre collaborateur sur son code afin de comprendre ses choix d’implémentation.
Échangez sur les modifications apportées au code par votre collaborateur pour prendre en compte les nouvelles fonctionnalités de la partie 2 et faites lui un retour sur la qualité de ce dernier livrable.
Puis, testez chacun l’exécution de plusieurs capteurs associés à un lecteur avec plusieurs analyseurs :
- définissez un programme par capteur (e.g.
run_sensor1.py
,run_sensor2.py
, etc.) et exécutez ces programmes dans des terminaux différents, - définissez un programme pour exécuter le lecteur.
Pour aller plus loin
Pour simuler l’indépendance des capteurs et du lecteur, vous avez exécuté les programmes depuis différents terminaux, chaque terminal étant isolé dans un processus système.
Depuis un même programme Python, il est possible d’exécuter différents morceaux de code indépendants les uns des autres en utilisant des threads
. Des thread
sont également appelés processus légers et peuvent être exécutés dans un même processus mais en parallèle. Utilisez la documentation pour expérimenter ce parallélisme.
To go beyond
Faire communiquer deux programmes sur une même machine, vous verrez comment le faire en réseau en S6, n’est pas une chose aisée. Le recours à un fichier partagé tel que nous l’avons fait n’est vraiment pas l’idéal. De nombreuses tentatives de lecture occupent inutilement des ressources de calcul. Une méthode plus appropriée est d’avoir recours aux techniques de communication entre processus (Inter Process Communication). Vous pouvez consulter la documentation des IPC en Python pour voir notamment comment des techniques d’envoi de signaux permettraient de synchroniser des lectures et des écritures.