NSI Première

Python : documenter et tester


Dans la première activité sur les fonctions, nous avons vu une première façon de les utiliser : enregistrer du code à utiliser et les activer lorsqu'on veut que ce code s'active.

Nous avons ensuite vu la différence entre les vraies fonctions (avec return) et les procédures (sans return), même si Python ne fait pas la différence.

Le rajout des paramètres permet encore de rajouter de la flexibilité à l'ensemble.

Nous allons aujourd'hui étendre cette utilisation : nous allons documenter correctement les fonctions pour que les personnes puissent l'utiliser sans avoir à comprendre le code de la fonction en lui-même et nous allons utiliser cette documentation pour réaliser des tests automatiques.

Logiciel nécessaire pour l'activité : Python 3

1 - Documenter une fonction

Notion   de   Doc String

Méthode

  • Documenter une fonction à créer est un bon moyen de se mettre d'accord sur le travail à réaliser.
  • La documentation de votre code Python est entièrement centrée sur les docstrings.
    Ce sont des chaînes intégrées qui, lorsqu’elles sont correctement configurées, peuvent aider vos utilisateurs et vous-même avec la documentation de votre projet.
  • Avec docstrings, Python a également la fonction intégrée help () qui imprime les objets docstring sur la console.

  • Les tripes guillemets indiquent une chaîne multi-lignes.
    Tout ce qu'il y a entre l'ouverture et la fermeture des guillemets fait partie de la chaîne, y compris les retours chariot et les autres guillemets.
  • """
    Chaîne sur plusieurs
    lignes
    """
    

    Donc, tout ce qui se trouve entre les triples guillemets fait partie de la doc string de la fonction, qui décrit ce que fait cette fonction.
    Elle doit être la première chose déclarée dans une fonction (la première chose après les deux points).


Contenu de la documentation

On peut ainsi fournir une fonction en ne donnant que sa déclaration (et ses paramètres) et en spécifiant dans la documentation :

  • Ce qui fait la fonction de manière succinte

  • Les paramètres d’entrées et leur type :

      ::param n (int):: l'indice...

      :param n: l'indice...
      :type n: int

      @param n: (int) l'indice...

  • La sortie et son type :

      ::return(float):: moyenne...

      :return: moyenne...
      :rtype : float

      @return: (float) moyenne...

    Pas de sortie ? ::return: (None)   ou  :return: (None)  ou   @return: (None)

  • Les conditions d’utilisation et effets de bord : ::CU :: (La table)  ou   :CU : La table   ou  @CU : La table

  • Eventuellement des tests

Exemple

Une fonction non spécifiée
def f(n):
  x = 1
  y = 1
  l = [x]
  k = 0
  while k < n:
    x, y = y, x + y
    l.append(x)
    k += 1
  return l

Il est difficile de savoir ce qu’elle fait sans lire ou exécuter le code.

Avec des spécifications convenables
def fibonacci(n):
  '''
  Termes de la suite de Fibonacci jusqu'à l'indice n inclus
  @param n: (int) l'indice maximal voulu
  @return: (list) la liste des termes
  '''
  x = 1
  y = 1
  suite_fibonacci = [x]
  indice = 0
  while indice < n:
    x, y = y, x + y
    suite_fibonacci.append(x)
    indice += 1
  return suite_fibonacci

Cette fois on dispose d’éléments pour comprendre le code.

  1. Sa documentation en haut
  2. Des variables explicites

Intérêt

  • programmer : documenter AVANT d’écire le code donne un objectif clair
  • relire : les programmes complexes sont difficiles à comprendre. La documentation simplifie cette étape
  • collaborer : travailler à plusieurs demande de l’organisation et une documentation claire est indispensable

Documenter : un attendu

La documentation fait partie des éléments attendus et qui seront toujours évalués.

Si vous ne documentez pas vos fonctions, vous n’obtiendrez jamais le maximum des points.

Exercices

01° Modifier la fonction calculer_moyenne pour qu'elle renvoie la réponse attendue précisée dans les commentaires.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
def calculer_moyenne(note1, coef1, note2, coef2) : '''Renvoie la moyenne coefficientée des deux notes :: param note1(int/float) :: nombre dans [0 ; 20] :: param note2(int/float) :: nombre dans [0 ; 20] :: param coef1(int) :: supérieur à 0 :: param coef2(int) :: supérieur à 0 :: return (float) :: moyenne coefficientée, nombre dans [0; 20] ''' moyenne = 0 return moyenne moy = calculer_moyenne(20, 2, 10, 4) print(moy)

...CORRECTION...

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
def calculer_moyenne(note1, coef1, note2, coef2) : '''Renvoie la moyenne coefficientée des deux notes :: param note1(int/float) :: nombre dans [0 ; 20] :: param note2(int/float) :: nombre dans [0 ; 20] :: param coef1(int) :: supérieur à 0 :: param coef2(int) :: supérieur à 0 :: return (float) :: moyenne coefficientée, nombre dans [0; 20] ''' moyenne = (note1*coef1+ note2*coef2) / (coef1 + coef2) return moyenne moy = calculer_moyenne(20, 2, 10, 4) print(moy)

Dans la correction proposée, on peut se passer de la création de la variable locale avant l'envoi avec return : on peut directement fournir l'expression après le return :

return (note1*coef1 + note2*coef2) / (coef1 + coef2)

On peut aussi utiliser une fonction dans une fonction :

02° Analyser, modifier et adapter la fonction fournir_note qui demande à l'utilisateur une note via un input. Veuillez à respecter les spécifications. Cette fonction utilise la fonction note_valide, en ligne 28, pour vérifier si l'entrée utilisateur est correcte.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
# - - - - - # Déclaration des fonctions - - # - - - - - def note_valide(note) : '''Renvoie True si note est dans l'intervalle [ 0 ; 20 ] :: param note (int/float) :: la note à tester :: return (bool):: True si note dans [ 0 ; 20 ] ''' if note > 20 : return False elif note < 0 : return False else : return True def fournir_note(intitule) : '''Renvoie une note valide après demande via un input en boucle :: param intitule(str) :: intitulé qui apparaît dans la question :: return (float) :: la note valide :: CU :: (conditions d'utilisation) utilise la fonction note_valide pour vérifier si proposition est valide ''' proposition = -1 while (note_valide(proposition) == ???) : proposition = input(f"Veuillez fournir {intitule} (entre 0 et 20) ::: ") ??? return proposition # - - - - - # Programme principal # - - - - - note1 = fournir_note("la note du DS (coef 4)") note2 = fournir_note("la note de l'interro (coef 1)")

Voici une possibilité de correction si vous avez un problème :

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
# - - - - - # Déclaration des fonctions - - # - - - - - def note_valide(note) : '''Renvoie True si note est dans l'intervalle [ 0 ; 20 ] :: param note (int/float) :: la note à tester :: return (bool):: True si note dans [ 0 ; 20 ] ''' if note > 20 : return False elif note < 0 : return False else : return True def fournir_note(intitule) : '''Renvoie une note valide après demande via un input en boucle :: param intitule(str) :: intitulé qui apparaît dans la question :: return (float) :: la note valide :: CU :: utilise la fonction note_valide pour vérifier si proposition est valide ''' proposition = -1 while (note_valide(proposition) == False) : proposition = input(f"Veuillez fournir {intitule} (entre 0 et 20) ::: ") proposition = float(proposition) return proposition # - - - - - # Programme principal # - - - - - note1 = fournir_note("la note du DS (coef 4)") note2 = fournir_note("la note de l'interro (coef 1)")

On peut encore aller plus loin : avec trois fonctions, on peut ainsi parvenir à réaliser un programme qui demande 2 notes valides et qui en fait la moyenne :

03° Tester le programme pour vérifier qu'on parvient bien à assembler les différentes fonctions pour réaliser un programme plus complexe en trois lignes : 49-50-51.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52
# - - - - - # Déclaration des fonctions - - # - - - - - def calculer_moyenne(note1, coef1, note2, coef2) : '''Renvoie la moyenne coefficientée des deux notes :: param note1(int/float) :: nombre dans [0 ; 20] :: param note2(int/float) :: nombre dans [0 ; 20] :: param coef1(int) :: supérieur à 0 :: param coef2(int) :: supérieur à 0 :: return (float) :: moyenne coefficientée, nombre dans [0; 20] ''' return (note1*coef1 + note2*coef2) / (coef1 + coef2) def note_valide(note) : '''Renvoie True si note est dans l'intervalle [ 0 ; 20 ] :: param note (int/float) :: la note à tester :: return (bool):: True si note dans [ 0 ; 20 ] ''' if note > 20 : return False elif note < 0 : return False else : return True def fournir_note(intitule) : '''Renvoie une note valide après demande via un input en boucle :: param intitule(str) :: intitulé qui apparaît dans la question :: return (float) :: la note valide :: CU :: utilise la fonction note_valide pour vérifier si proposition est valide ''' proposition = -1 while (note_valide(proposition) == False) : proposition = input(f"Veuillez fournir {intitule} (entre 0 et 20) ::: ") proposition = float(proposition) return proposition # - - - - - # Programme principal # - - - - - note1 = fournir_note("note du DS (coef 4)") note2 = fournir_note("note de l'interro (coef 1)") moyenne = calculer_moyenne(note1, 4, note2, 1) print(moyenne)

Pas mal non ? Et encore, nous verrons plus tard qu'on peut enregister nos fonctions dans nos propres modules et qu'on peut alors simplifier le programme qui pourrait se résumer à ceci :

1 2 3 4 5 6
import mesfonctions note1 = mesfonctions.fournir_note("note du DS (coef 4)") note2 = mesfonctions.fournir_note("note de l'interro (coef 1)") moyenne = mesfonctions.calculer_moyenne(note1, 4, note2, 1) print(moyenne)

Ou encore mieux (car on n'a pas besoin de préciser à chaque fois le nom du module personnel) :

1 2 3 4 5 6
from mesfonctions import * note1 = fournir_note("note du DS (coef 4)") note2 = fournir_note("note de l'interro (coef 1)") moyenne = calculer_moyenne(note1, 4, note2, 1) print(moyenne)

2 - Doctest

Nous allons maintenant voir comment on peut utiliser la documentation pour préciser un peu plus

  • comment on veut utiliser les fonctions et
  • comment vérifier qu'elle fonctionne correctement

Cette façon de procéder sera au coeur de votre notation désormais.

Premièrement, sachez qu'on peut fournir des exemples d'utilisation de la fonction dans la documentation elle-même. Dans l'exemple ci-dessous, on fournit un exemple d'utilisation de la fonction via le Shell Python, entre les lignes 12 et 19 :

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
def note_valide(note) : '''Renvoie True si note est dans l'intervalle [ 0 ; 20 ] :param note: la note à tester :type note: int or float :return:: True si note dans [ 0 ; 20 ] :rtype: bool :Exemple: >>> note_valide(5) True >>> note_valide(-5) False >>> note_valide(25) False >>> note_valide(20.0) True ''' if note > 20 : return False elif note < 0 : return False else : return True

04° Est-on capable de créer le doctest AVANT que la fonction ne soit réellement fonctionnelle ? Comment montrer au codeur qu'on désire que 20 ou 20.0 fournisse True dans les deux cas ?

...CORRECTION...

Oui, on peut écrire les résultats attendus de notre fonction avant même qu'elle ne soit réellement codée. Il suffit de réflechir à ce qu'elle devrait renvoyer.

Pour que le doctest soit clair vis à vis du 20 integer ou du 20 float, il suffit de rajouter un test dans le doctest :

>>> note_valide(20.0) True >>> note_valide(20) True

Pour l'instant, il ne s'agit que d'une simple documentation. Mais nous allons voir un module de Python qui nous permet de l'utiliser : le module va alors déclencher les tests.

Module doctest

Pour importer le module, il suffit de faire comme pour n'importe quel module :

import doctest

L'utilisation commune de doctest est de ne l'utiliser que si vous activez directement votre fichier Python (en opposition avec un appel via un import). Pour cela, il suffit de rajouter ceci à la fin de votre fichier Python :

if __name__ == "__main__": import doctest doctest.testmod()

Ce code va permettre de savoir

  • si votre fichier a bien été activé directement par l'utilisateur (dans ce cas __name__ vaut bien "__main__")
  • ou s'il a été activé via un import

05° Utiliser le code suivant qui ne contient que la fonction (fausse, voir ligne 26) et l'activation du doctest. Observer s'il se passe quelque chose.

Modifier alors la fonction de façon à passer les tests.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
# - # Déclaration des fonctions # - def note_valide(note) : '''Renvoie True si note est dans l'intervalle [ 0 ; 20 ] :param note: la note à tester :type note: int or float :return:: True si note dans [ 0 ; 20 ] :rtype: bool :Exemple: >>> note_valide(5) True >>> note_valide(-5) False >>> note_valide(25) False >>> note_valide(20.0) True ''' if note > 30 : return False elif note < 0 : return False else : return True # - # Corps du programme # - if __name__ == "__main__": import doctest doctest.testmod()

...CORRECTION...

On observe bien qu'il se passe un problème avec le test sur le 25 :

********************************************************************** File "/home/rv/test5.py", line 20, in __main__.note_valide Failed example: note_valide(25) Expected: False Got: True ********************************************************************** 1 items had failures: 1 of 4 in __main__.note_valide ***Test Failed*** 1 failures.

Lorsqu'on repasse les tests après avoir correctement modifié le IF de la ligne 26, on constate qu'il ne se passe plus rien. C'est l'effet attendu :

Intéret de la mise en place de tests

A l'aide des tests, on peut immédiatement s'apercevoir après avoir fait une correction à un endroit du code que notre correction vient de casser une autre fonctionnalité ailleurs dans le code.

Le principe est donc de modifier quelques lignes de code, de relancer le fichier et de voir si les tests sont toujours corrects. Sinon, on voit clairement apparaître le problème.

Attention : la ligne vide en dessous de dernier test doit bien être une ligne vierge. Sans cela, les tests ne font pas fonctionner. Rajouter donc toujours cette fameuse ligne vide.

Il est bien entendu possible de rendre le test visible, même en cas de réussite :

06° Modifier l'appel à la fonction testmod contenu dans le module doctest. Relancer le fichier.

doctest.testmod(verbose=True)

Cette fois, on constate bien que les tests sont réalisés, même s'ils sont ok.

Trying: note_valide(5) Expecting: True ok Trying: note_valide(-5) Expecting: False ok Trying: note_valide(25) Expecting: False ok Trying: note_valide(20.0) Expecting: True ok 1 items had no tests: __main__ 1 items passed all tests: 4 tests in __main__.note_valide 4 tests in 2 items. 4 passed and 0 failed. Test passed.

On voit qu'il fait le bilan des tests, qu'il donne clairement les fonctions qui n'ont pas de tests ( __main__ ici, nous en parlerons pendant l'activité sur les modules).

D'autres informations sur l'utilisation des doctests sont disponibles dans l'activité sur les tests unitaires. Pour l'instant, cela suffira à vous montrer l'utilité de ces tests.

D'ailleurs, on force rarement verbose à True car dans ce cas, cela bloque le fonctionnement de la fonction à ce format. Nous verrons d'autres façons de faire appel aux doctests, en ayant la possibilité de fournir beaucoup d'options de déroulement.

07° Dans le programme ci-dessous, quelles sont les fonctions qui seront signalées par la fonction testmod comme n'ayant pas de doctests ? Créer quelques tests supplémentaires sur la première à apparaitre dans le code, de façon à limiter le nombre de fonctions non testées.

Attention : les tests fournis doivent impérativement respecter l'exacte codification des réponses attendues. Comme les fonctions sont déjà créées, vous pouvez faire un copier-coller des réponses obtenues dans le Shell.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61
# - # Déclaration des fonctions # - def calculer_moyenne(note1, coef1, note2, coef2) : '''Renvoie la moyenne coefficientée des deux notes :: param note1(int/float) :: nombre dans [0 ; 20] :: param note2(int/float) :: nombre dans [0 ; 20] :: param coef1(int) :: supérieur à 0 :: param coef2(int) :: supérieur à 0 :: return (float) :: moyenne coefficientée, nombre dans [0; 20] ''' return (note1*coef1 + note2*coef2) / (coef1 + coef2) def note_valide(note) : '''Renvoie True si note est dans l'intervalle [ 0 ; 20 ] :: param note (int/float) :: la note à tester :: return (bool):: True si note dans [ 0 ; 20 ] :: exemples :: >>> note_valide(5) True >>> note_valide(-5) False >>> note_valide(25) False >>> note_valide(20.0) True ''' if note > 20 : return False elif note < 0 : return False else : return True def fournir_note(intitule) : '''Renvoie une note valide après demande via un input en boucle :: param intitule(str) :: intitulé qui apparaît dans la question :: return (float) :: la note valide :: CU :: utilise la fonction note_valide pour vérifier si proposition est valide ''' proposition = -1 while (note_valide(proposition) == False) : proposition = input(f"Veuillez fournir {intitule} (entre 0 et 20) ::: ") proposition = float(proposition) return proposition # - # Corps du programme # - if __name__ == "__main__": import doctest doctest.testmod(verbose=True)

La documentation des fonctions est une bonne pratique à acquérir. L'utilisation de tests l'est également. Dans le cadre de Python, l'utilisation de tests via les doctests est parfois bien vue, parfois non. Il existe des façons plus professionnelles de faire des tests unitaires. Mais le doctest à l'avantage de la simplicité.

L'un des probèmes de doctest est notamment le cas des fonctions non déterministes : les fonctions intégrant une part d'aléatoire ou les fonctions intégrant des inputs. Dans ce cas, les tests deviennent beaucoup moins naturels et ils demandent d'écrire les fonctions un peu différement.

Dans le cadre de notre programme, nous avons en plus le problème du while qui oblige à fournir non pas un mais plusieurs faux résultats de inputs lors des tests...

08° Hors programme en NSI. C'est très spécifique : il s'agit de savoir faire des tests sur une fonction qui demande un input. Vous pouvez passer si vous voulez rester sur des choses simples. Sinon, observer la façon dont nous avons ici réalisé des tests avec des simulations de input dans le doctest de la fonction fournir_note. Cela vous permettra de réaliser de nouveaux tests sur vos fonctions comportant des inputs. Les explications viendront dans la prochaine activité sur les fonctions.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92
# - # Déclaration des fonctions # - def calculer_moyenne(note1, coef1, note2, coef2) : '''Renvoie la moyenne coefficientée des deux notes :param note1: nombre dans [0 ; 20] :type note1: int or float :param coef1: nombre supérieur à 0 :type coef1: int :param note2: nombre dans [0 ; 20] :type note2: int or float :param coef2: nombre supérieur à 0 :type coef2: int :return: moyenne coefficientée, nombre dans [0; 20] :rtype: float :Exemple: >>> calculer_moyenne(20, 2, 10, 2) 15.0 >>> calculer_moyenne(20, 3, 10, 1) 17.5 ''' return (note1*coef1 + note2*coef2) / (coef1 + coef2) def note_valide(note) : '''Renvoie True si note est dans l'intervalle [ 0 ; 20 ] :param note1: la note à tester :type note: int or float :return: True si note dans [0; 20] :rtype: bool :: exemples :: >>> note_valide(5) True >>> note_valide(-5) False >>> note_valide(25) False >>> note_valide(20.0) True ''' if note > 20 : return False elif note < 0 : return False else : return True def fournir_note(intitule) : '''Renvoie une note valide après demande via un input en boucle :param intitule: intitulé qui apparaît dans la question :type intitule: str :return: une note valide fournie par l'utilisateur :rtype: float :Exemple: >>> import builtins >>> builtins.input = lambda *x, **y: '15' >>> fournir_note('la note de test') 15.0 >>> builtins.input = lambda *x, **y: '15.0' >>> fournir_note('la note de test') 15.0 .. note:: utilise la fonction note_valide pour vérifier si proposition est valide ''' proposition = -1 while (note_valide(proposition) == False) : proposition = input(f"Veuillez fournir {intitule} (entre 0 et 20) ::: ") proposition = float(proposition) return proposition # - # Corps du programme # - if __name__ == "__main__": import doctest doctest.testmod(verbose=True)

Dans le cadre du doctest de la fonction fournir_note, je n'ai mis que des réponses valides dès le premier coup à cause de la présence du while. Ma façon de gérer les strings avec les doctests ne permet pas de fournir plusieurs réponses simulées de input à la suite.

Les docstests permettent donc d'éviter les bugs. Surtout ceux liés à des modifications de code qui fonctionnaient, avant modification ! Mais qui a inventé le terme ?

Beaucoup de gens attribuent faussement l'invention du mot à Grace Hopper.

3 - Assertion

La documentation n'est en aucun cas une contrainte pour quelqu'un qui veut faire appel à votre fonction.

Imaginons qu'on ai besoin d'une fonction transforme un pourcentage (exprimé entre 0 et 1) en un nombre entier exprimé entre 0 et 100.

1 2 3 4 5 6 7 8
def transforme(pourcentage) : '''Transforme un pourcentage (de 0 à 1) en un entier compris entre 0 et 100 :: param pourcentage(float) :: un flottant compris dans l'intervalle [0;1] :: return (int) :: un entier compris entre 0 et 100 ''' return int(pourcentage*100)

La documentation impose clairement de n'envoyer que des flotants dans [0;1]. Enfin, impose... Non : la documentation explique qu'il ne faut envoyer qu'un flottant dans [0;1].

Voyons ce que se passe si on passe outre :

>>> transforme(1.12) 112 >>> transforme(-0.01) -1

Et oui : aucune erreur. La fonction renvoie pourtant des valeurs non prévues puisque les entrées ne sont pas dans l'intervalle attendu.

Imaginons maintenant que votre fonction fasse partie d'un processus de démarrage d'un système critique (une fusée ? Une centrale nucléaire ?). Dans ce cas, nous aimerions que le système stoppe et signale le problème non ?

Et c'est possible en utilisant par exemple les assertions.

Assertions avec assert

Il arrive parfois qu'on désire détecter les cas non prévus ou stopper un processus avant qu'il ne puisse causer de dommages en envoyant des données erronées.

Pour cela, on utilise le mot-clé assert.

Son utilisation basique se résume à une succession de trois éléments :

  1. Le mot-clé assert
  2. Un espace suivi de la condition qu'on veut vérifier sous forme d'une expression booléenne
  3. Une virgule suivi du texte à afficher si l'expression est évaluée à False

En résumé :

1
assert note >= 0 and note <= 20, "Votre note n'est pas dans l'intervalle [0;20] !"

Cette assertion peut donc provoquer l'arrêt du programme et signaler la raison à l'utilisateur en affichant le texte. Ce cas arrive si la condition n'est pas évaluée à True.

Exemple :

>>> note = 15 >>> assert note >= 0 and note <= 20, "Votre note n'est pas dans l'intervalle [0;20] !" >>> note = 25 >>> assert note >= 0 and note <= 20, "Votre note n'est pas dans l'intervalle [0;20] !" Traceback (most recent call last): AssertionError: Votre note n'est pas dans l'intervalle [0;20] !

On voit que le message est clair.

D'ailleurs, il s'agit du même type de message lorsque c'est l'interpréteur qui détecte une erreur :

>>> note = 'a' >>> assert note >= 0 and note <= 20, "Votre note n'est pas dans l'intervalle [0;20] !" Traceback (most recent call last): TypeError: '>=' not supported between instances of 'str' and 'int'
13° Rajouter à la fonction pourcentage une assertion permettant de vérifier la validité de la précondition sur le cas >=0 puis sur le cas <=1.

Pour l'instant, on impose juste par une assertion que le paramètre soit bien un entier ou un flottant.

1 2 3 4 5 6 7 8 9 10
def transforme(pourcentage) : '''Transforme un pourcentage (de 0 à 1) en un entier compris entre 0 et 100 :: param pourcentage(float) :: un flottant compris dans l'intervalle [0;1] :: return (int) :: un entier compris entre 0 et 100 ''' assert isinstance(pourcentage, float) or isinstance(pourcentage, int), "pourcentage n'est pas un entier ou un flottant" return int(pourcentage*100)

...CORRECTION...

1 2 3 4 5 6 7 8 9 10 11 12
def transforme(pourcentage) : '''Transforme un pourcentage (de 0 à 1) en un entier compris entre 0 et 100 :: param pourcentage(float) :: un flottant compris dans l'intervalle [0;1] :: return (int) :: un entier compris entre 0 et 100 ''' assert isinstance(pourcentage, float) or isinstance(pourcentage, int), "pourcentage n'est pas un entier ou un flottant" assert pourcentage >=0, "pourcentage inférieur à 0 !" assert pourcentage <=1, "pourcentage supérieur à 1 !" return int(pourcentage*100)

14° Rajouter à la fonction pourcentage une assertion permettant de vérifier la validité de la postcondition.

1 2 3 4 5 6 7 8 9 10
def transforme(pourcentage) : '''Transforme un pourcentage (de 0 à 1) en un entier compris entre 0 et 100 :: param pourcentage(float) :: un flottant compris dans l'intervalle [0;1] :: return (int) :: un entier compris entre 0 et 100 ''' reponse = int(pourcentage*100) return reponse

...CORRECTION...

1 2 3 4 5 6 7 8 9 10 11
def transforme(pourcentage) : '''Transforme un pourcentage (de 0 à 1) en un entier compris entre 0 et 100 :: param pourcentage(float) :: un flottant compris dans l'intervalle [0;1] :: return (int) :: un entier compris entre 0 et 100 ''' reponse = int(pourcentage*100) assert reponse >=0 and reponse <=100, "La réponse n'est pas dans l'intervalle [0;100]. Problème." return reponse

Le module doctest est vraiment pratique pour créer des documentations qui servent aussi à réaliser des jeux de tests.

Mais on peut aussi facilement réaliser des jeux de tests avec d'autres modules (pytest ou unitest).

Finalement, on peut même se passer de ces modules et n'utiliser que des asserts contenus dans une fonction qu'on lancera pour tester nos fonctions.

1 2 3 4 5 6
def addition(a, b) : return a*b def test_maison() : assert addition(10,5) == 15, 'Echec du test : addition(10,5) == 15' assert addition(10,-5) == 5, 'Echec du test : addition(10,-5) == 5'

15° Mettre les deux fonctions précédentes en mémoire.

Lancer les tests en tapant ceci dans le Shell :

>>> test_maison()

Vous devriez détecter une erreur. A vous de regarder la fonction d'addition pour trouver d'où cela vient.

Et on ne peut pas l'activer automatiquement au lancement ?

Si, il suffit de faire comme avec doctest par exemple : on lancera simplement test_maison.

1 2 3 4 5 6 7 8 9
def addition(a, b) : return a+b def test_maison() : assert addition(10,5) == 15, 'Echec du test : addition(10,5) == 15' assert addition(10,-5) == 5, 'Echec du test : addition(10,5) == 15' if __name__ == '__main__' : test_maison()

On retiendra qu'il faut documenter vos fonctions même s'il ne s'agit que d'une simple indication. On y indique les types des paramètres et du retour, ainsi que les préconditions et les postconditions. Attention : il ne s'agit que de simples textes indicatifs, rien de contraignant pour quelqu'un qui voudrait "casser" le système.

Si on veut imposer l'arrêt du programme en cas de non respect des préconditions ou d'échec de la postcondition, on peut alors utiliser des assertions, créées par le mot-clé assert en Python.

Enfin, on peut réaliser des jeux de tests, automatisés ou pas, de façon à tester les fonctionnalités de la fonction. Cela permet de vérifier qu'une modification mineure pour améliorer une fonctionnalité, n'a pas dégradé une autre fonctionnalité.

Pour réaliser ces tests, on peut utiliser :

  • une fonction indépendante contenant des assertions et testant vos fonctions
  • le module doctest qui a l'avantage de l'automatisation et d'utiliser les exemples d'utilisation qu'on place dans la documentation !
  • d'autres modules encore plus puissants comme pytest ou unitest. Ces modules permettent de réaliser des tests sur tout un ensemble de programmes. On nomme cela des tests unitaires.

Attention néanmoins à bien comprendre cette citation d'Edsger Dijkstra : « Le test de programmes peut être une façon très efficace de montrer la présence de bugs, mais est désespérément inadéquat pour prouver leur absence ».

Il faut donc bien avoir en mémoire que les jeux de tests ne permettent que de détecter des problèmes particuliers éventuels, pas de tous les détecter.

4 - FAQ

Du coup, on peut tester le type des variables ?

Deux manières faciles de faire cela :

  • Utiliser la fonction native isinstance qui permet de vérifier qu'une variable fait bien référence à un type particulier de contenu :
  • >>> isinstance (5, int) True >>> isinstance (5, float) False >>> isinstance (5, str) False

    Voilà qui permet d'expliquer cette ligne donc :

    8
    assert isinstance(pourcentage, float) or isinstance(pourcentage, int), "pourcentage n'est pas un entier ou un flottant"

    Deuxième méthode que nous avions vu : tester le type de la variable.

    >>> a = 5.0 >>> type(a) == float True >>> type(a) == int False >>> type(a) == str False >>> a = "5.0" >>> type(a) == float False >>> type(a) == int False >>> type(a) == str True

    Ca peut donc donner ceci :

    8
    assert type(pourcentage) == float, "pourcentage n'est pas un flottant"

Si on récapitule, nous avons vu sur différentes activités :

  • la différence entre fonction (avec retour), et procédure (sans retour)
  • les paramètres des fonctions
  • la portée des variables interagissant avec les fonctions
  • la documentation
  • les tests
  • (les doctests permettent de fournir une documentation utilisée pour tester les fonctions)
  • la structuration d'un programme complexe en multiples petites fonctions

Activité publiée le 3 09 2020
Auteur : ows. h. modifié : Andjekel