Analyse des programmes
Au début des années 1980 circulait sur le réseau Usenet, précurseur des forums Internet, un texte parodique commençant par les lignes :
Real Programmers don't write specs - users should consider themselves lucky to get any programs
at all, and take what they get.
Real Programmers don't comment their code. If it was hard to write, it should be hard to understand.
...
(
pour la suite voir par exemple ici ... )
Dans un contexte où l'informatique prenait de plus en plus d'importance, ce texte se moquait gentiment de la résistance de certains vétérans aux bonnes pratiques imposées par la professionnalisation de l'activité de programmeur.... car bien entendu, les utilisateurs de logiciels ne sont pas très disposés à se contenter de ce qu'on leur donne quelle que soit sa qualité, surtout quand les enjeux sont importants.
Les principaux critères de qualité d'un programme peuvent être regroupés selon trois axes principaux:
- le programme doit exécuter les tâches attendues, sans erreurs ni 'plantage'
- le programme doit pouvoir s'exécuter en une durée raisonnable sur une configuration matérielle raisonnable.
- son code doit être facile à corriger et à faire évoluer
(on peut y ajouter des critères d'ergonomie et d'esthétique mais qui sont l'affaire d'autres spécialistes que les programmeurs).
Le premier axe se rapporte à ce qu'on appelle la
correction des programmes , le second à ce qu'on appelle leur
complexité . Le dernier est associé aux bonnes pratiques d'écriture, qui peuvent varier un peu selon les langages et paradigmes de programmation, mais dont les principes de base restent de commenter son code et d'utiliser des noms significatifs pour les variables et les fonctions.
Correction des programmes
Notion de correction
De manière schématique, on peut considérer un programme comme une 'boîte noire' prenant des données en entrée et fournissant un résultat.
.
On peut appliquer la même description à une fonction, qui est au fond un petit programme. Dans les exemples, nous travaillerons essentiellement avec des fonctions.
Dans le cas de la fonction, les données sont les valeurs passées en paramètrès. Le résultat peut être constitué des valeurs de retour s'il y en a, ou par un changement d'état provoqué quelque part par la fonction (afficher un message, ouvrir un fichier...).
Que peut-il mal se passer?
- Le programme, ou la fonction, peut 'planter', se bloquer (par exemple entrer dans une boucle infinie) : on dit alors qu'il ne 'termine' pas.
- Si le programme, ou la fonction, termine, le résultat peut différer de celui attendu pour certaines données (ou pour toutes!). On dit alors que le programme ou la fonction n'est pas correct
Spécification
Pour pouvoir s'assurer qu'un programme effectue bien la tâche voulue, il est important de commencer par définir et délimiter cette tâche. C'est ce qu'on appelle la
spécification du programme (en abrégé 'spec', ce que le 'Real Programmer' refusait d'écrire ...).
La spécification d'un programme est la description précise du résultat que l'on attend de ce programme.
Elle précisera généralement des conditions et restrictions sur les données que le programme devra être capable de traiter et les résultats qu'il devra produire.
La spécification est évidemment primordiale dans le cas où le programme doit répondre à une commande, que ce soit d'un client à un fournisseur, d'un coordonnateur de projet à une équipe, ou d'un professeur à un élève.
Mais même pour un projet personnel, réfléchir à la spécification avant de commencer à écrire fait gagner en temps, en efficacité et généralement en qualité.
Vérification de la correction
Il n'existe pas d'outils universel permettant de
prouver la correction de n'importe quel programme pour toutes les données possibles.
Il est toutefois possible d'effectuer un certain nombre de vérifications, permettant d'offrir quelques garanties.
On peut tester expérimentalement le fonctionnement du programme sur certains ensembles de données, pour contrôler si le résultat est conforme à la spécification et si le programme ne produit pas d'erreurs. Ces tests peuvent eux-mêmes être réalisés par des programmes.
On peut aussi analyser le code en amont pour vérifier s'il répond à certains critères.
Analyse de fonctions
Prototype d'une fonction
On appelle "prototype" d'une fonction l'ensemble constitué
- de son nom
- de la liste de ses paramètres, avec leurs caractéristiques (types, obligatoire ou non, de position ou nommé)
- du nombre de ses valeurs de retour, avec leurs types.
Par exemple, pour la fonction Python int, le prototype pourrait s'écrire :
- nom : int
- paramètres :
- un paramètre de position obligatoire pouvant être un nombre ou une chaîne de caractères
- un paramètre nommé 'base' facultatif, de type entier, valant 10 par défaut.
- valeur de retour : une valeur de type entier
Intérêt de la notion de prototype
Les indications fournies par le prototyps sont celles qui sont absolument nécessaires pour utiliser une fonction. La liste des prototypes constitue la documentation minimale à fournir pour une bibliothèque de fonctions.
Lors de la conception d'un programme dont plusieurs fonctions peuvent s'appeler l'une l'autre, définir tous les prototypes avant de commencer l'implémentation de chaque fonction permet de gagner du temps et d'éviter les erreurs ultérieures
Dans certains langages, généralement des langages compilés à typage statique, le prototype de la fonction doit être déclaré préalablement, et parfois séparément, à son implémentation.
Explicitation du prototype en Python
Dans la déclaration d'une fonction en Python, il est possible, de manière facultative, de déclarer le type des paramètres et de la valeur de retour de la fonction.
- Pour déclarer le type d'un paramètre, on fait suivre le nom du paramètre de ':' suivi du nom du type. Si le paramètre a une valeur par défaut, elle se place après la déclaration de type
- Pour déclarer le type de la valeur de retour, on place, après la fermeture des parenthèses contenant les paramètres, les signes '->' suivis du nom de type
Par exemple :
- def aleatoire(taille:int,max:int=100)->list:
Cette déclaration traduit le prototype suivant :
La fonction
aleatoire prend un paramètre obligatoire de type entier et un paramètre facultatif nommé 'max', de type entier. Elle retourne une liste.
Ces déclarations n'ont qu'une valeur informative : aucun contrôle n'est effectué par l'interpréteur Python pour vérifier si la fonction est appelée avec les bons types de paramètres, ou si elle renvoie effectivement le type annoncé.
Postconditions et préconditions
Connaître le prototype d'une fonction est suffisant pour pouvoir l'appeler dans un programme, mais ne permet pas de savoir à quoi elle pourrait être utile !
Postconditions
Pour préciser la spécification de la fonction, on doit indiquer des
postconditions , c'est à dire les conditions qui doivent être remplies lorsque l'exécution de la fonction est terminée.
Les postconditions incluent l'objectif premier de la fonction. Par exemple pour la fonction Python int , la postcondition pourrait s'écrire :
"la valeur de retour de la fonction est un nombre entier, égal à la valeur arrondie au plus proche du paramètre obligatoire s'il s'agissait d'un nombre, ou bien, s'il s'agissait d'une chaîne de caractères, à la valeur représentée par cette chaîne de caractères dans la base numérique indiquée par le paramètre base".
Préconditions
Connaître les caractéristiques des paramètres d'une fonction ne suffit pas forcément à ce que son utilisation n'engendre pas d'erreur.
Il est fréquent de rencontrer des limitations sur les valeurs que l'on peut donner aux paramètres (arguments).
Par exemple la fonction ci-dessous causera une erreur d'exécution si on l'appelle avec un argument nul.
- def inverse(x):
- return 1/x
Pour éviter ce type d'erreurs, on peut préciser des
préconditions qui devront être remplies lors de l'appel de la fonction.
Les préconditions portent souvent sur les domaines de valeurs possibles pour les arguments, mais elles peuvent également porter sur tout autre état du système (existence ou ouverture d'un fichier, valeur d'une variable globale, répertoire de travail du système, connexion au réseau...).
La spécification d'une fonction est constituée de son prototype, et de ses préconditions et postconditions
Aider l'utilisateur : la docstring
Connaître les préconditions et postconditions est très utile à qui veut utiliser une fonction venant par exemple d'une bibliothèque. Python dispose d'un outil pour permettre à l'auteur de la fonction de documenter celle-ci directement dans le code : la docstring.
La docstring d'une fonction Python est une chaîne de caractère encadrée par des guillemets triples ''' ou """, pouvant s'écrire sur plusieur lignes, et placée juste sous l'en-tête de la fonction.
Par convention, la première ligne de la docstring est une phrase brève indiquant ce que fait la fonction. Des précisions peuvent être apportées, après un saut de ligne, dans la suite de la docstring.
La docstring d'une fonction peut être affichée dans la console en utilisant la fonction help() avec comme paramètre le nom de la fonction.
Mise au point et vérification
Utilisation d'assertions
L'instruction assert permet de vérifier si une condition est remplie à un moment donné de l'exécution d'un programme, et d'indiquer le message d'erreur à afficher sinon. Sa syntaxe est assert condition, message où condition est une variable logique et message une chaîne de caractères.
Exemple : en mode interactif , l'instruction >>>assert 2==3, 'apprends à compter' produit l'affichage
Traceback (most recent call last):
File "", line 1, in
AssertionError: apprends à compter
Essentiellement utilisée lors de la mise au point des programmes, elle peut être utilisée par exemple pour vérifier que l'appel à une fonction respecte son prototype et ses préconditions, ou bien que les postconditions de la fonction sont bien réalisées.
Exemple :
- def serie(i):
- '''
- prend comme paramètre un entier i positif non nul
- affiche par ordre croissant tous les entiers positifs compris entre 1 et i+1
- '''
- assert isinstance(i,int) and i>0 , 'le paramètre doit être un entier positif'
- for i in range(0,i):
- print(i+1,end=' ')
L'assertion est utilisée ici - d'une part pour vérifier que le type du paramètres est correct, grâce à la fonction isinstance .( isinstance prend comme paramètres une variable, et un nom de type, et renvoie True si le premier paramètre est du type indiqué, False sinon)
- d'autre part pour vérifier le respect de la précondition "paramètre positif non nul".
Programmes de test
Vérifier expérimentalement la conformité d'un élément de programme à sa spécification nécessite d'exécuter cet élément pour un certain nombre de configurations initiales.
Il s'agit d'une vérification expérimentale : les tests vont exécuter le code concerné dans plusieurs cas de figure, et vérifier si le résultat est conforme à la spécification.
Cela peut vite devenir fastidieux, surtout quand les tests doit être répété pendant la phase de mise au point. Il peut être intéressant d'automatiser le processus en écrivant des fonctions de test.
Une fonction de test peut simplement comparer, pour quelques exemples, le résultat attendu et le résultat obtenu. L'utilsation de l'instruction assert est recommandée.
Exemple : La fonction test_max ci-dessous permet de tester la fonction Python max : si tout se passe bien, la fonction de test termine sans erreur. Si un des tests échoue, une erreur d'assertion se produit, et l'assertion non vérifiée est affichée dans le message d'erreur.
- def test_max():
- assert max(1,2)==2
- assert max([1,2])==2
- assert max([-1,2])==2
- assert max([-1,-2])==-1
- assert max((12,))==12
- assert max('a','z')=='z'
- test_max()
(exécuter ce programme après avoir modifié une des assertions pour qu'elle soit fausse, pour voir le résultat)
Un test peut également comparer des propriétés bien choisies du résultat obtenu, avec celles attendues selon la spécification de la fonction.
Supposons par exemple qu'on veuille tester la fonction Python sorted pour un seul argument (on rappelle que sorted prend un seul paramètre obligatoire, qui est la séquence à trier, et renvoie une liste contenant les éléments de la séquence, par défaut triés dans leur ordre croissant).
On pourra vérifier si, en appliquant sorted sur quelques exemples de séquences effectivement 'triables' (c'est à dire dont tous les élément peuvent être comparés entre eux)
- les éléments de la liste obtenue sont bien rangés dans l'ordre croissant.
- tous les éléments de la séquence passée en paramètre se retrouvent bien dans la liste obtenue.
- la liste obtenue ne contient pas d'éléments qui n'étaient pas présents dans la séquence de départ
exemple de programme de test :
- def test_sorted():
- for sequence in [
-
(0,-3,1,100),
-
['a','z','e','r','t','y'],
- (True,False,True),
-
[3.25,-4.0,0.1,-1.5,
4.5,-57.58],
- []
-
]:
- resultat=sorted(sequence)
- for
elt in sequence:
-
assert elt in resultat, 'élément absent'
- for elt in resultat:
- assert elt in sequence, 'élément en trop'
- for i in range(0,len(resultat)-1):
- assert resultat[i]<=resultat[i+1], 'pas ordonné'
- return
La fonction
sorted est ainsi testée sur quelques exemples de séquences (listes et tuples de contenus divers).
Un ensemble de tests ne donne qu'une preuve partielle de correction du code : ce n'est pas parce que le programme est correct pour les cas vérifiés dans les test, qu'il le sera dans tous les cas possibles.
Les tests seront d'autant plus probant qu'ils porteront sur des cas bien choisis, nombreux et suffisamment diversifiés, incluant les cas particullers.