python tests

Identification

Infoforall

20 - doctests : documenter et tester


Nous avons vu plusieurs choses sur les fonctions :

  • Comment déclarer une fonction pour la placer en mémoire.
  • Comment lancer un appel à une fonction déjà en mémoire
  • Comment différencier variables globales et variables locales.
  • Que les arguments sont les données qu'on envoie en entrée d'une fonction.
  • Que les paramètres sont les variables locales qui pourront stocker les arguments envoyés.
  • Que les fonctions doivent disposer d'une documentation (souvent nommée docstring en Python) permettant de connaître le contrat de confiance de cette fonction : quelles sont les préconditions à respecter pour bénéficier des postconditions.

Nous allons voir aujourd'hui qu'on peut tester automatiquement qu'une fonction fonctionne correctement, ou pas.

En terme de culture générale sur l'informatique, vous allez découvrir qu'on peut clairement créer un système permettant de définir les responsabilités de chacun lorsqu'un problème survient sur un système informatique :

  • Est-ce l'utilisateur de la fonction ?
  • Est-ce le concepteur de la fonction ?
  • Est-ce le supérieur du concepteur de la fonction qui lui a confié cette mission ?

Bien entendu, tout ceci sera transposable à d'autres milieux : assurance, milieu médical, monde de l'édition....

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

Evaluation ✎ : questions 07-

Résumé : Version HTML ou fond blanc ou ou PDF (couleur ou gris)

1 - Tester manuellement

01° Placer la fonction sur100() en mémoire puis répondre aux questions proposées.

1 2 3 4 5 6 7 8 9 10 11 12 13
def sur100(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 :: exemples .. >>> sur100(0.5) 50 >>> sur100(0.05) 5 ''' return int(pourcentage*100)
  1. Sur quelle ligne peut-on apprendre ce que réalise globalement cette fonction ?
  2. En étudiant les préconditions fournies sur la documentation L4, quel doit-être le type de ce qu'on va stocker dans le paramètre pourcentage ?
  3. En étudiant la postcondition visible L5, quel doit-être le type du résultat renvoyée ?
  4. Selon l'exemple des lignes 7-8, quel est le résultat attendu si on lance un appel avec l'argument 0.5 ?
  5. Réaliser les exemples fournis dans la documentation pour vérifier qu'ils correspondent à la réalité de ce que fait la fonction.
  6. >>> sur100(0.5) ??? >>> sur100(0.05) ???
  7. Notez bien la présence d'une ligne vide (L11) après le dernier exemple et la suite de la documentation.

...CORRECTION...

  1. Il suffit de regarder sous la ligne du prototype. Ligne 2.
  2. En lisant la L4, on voit qu'on doit envoyer un flottant. On peut même voir qu'il devrait être compris entre 0.0 et 1.0.
  3. En étudiant la postcondition visible L5, on voit que la fonction va renvoyer un entier.
  4. Il suffit de lire : 50
  5. On tape ceci :
  6. >>> sur100(0.5) 50 >>> sur100(0.05) 5

Nous allons maintenant revenir sur cela :

02° Répondre aux questions (et lancer les appels éventuels pour voir le résultat) :

  1. Un utilisateur peut-il envoyer des arguments ne respectant pas les préconditions ?
  2. Sur l'appel B, la fonction ne respecte pas la postcondition d'un résultat compris entre 0 et 100. Est-ce la faute du concepteur de la fonction ou de l'utilisateur ?
  3. Appel B

    >>> a = sur100(1.12) >>> a 112
  4. Sur l'appel C, l'utilisateur ne respecte pas l'une des précondtions. Laquelle ? Le non-respect de la précondition entraîne-t-elle nécessairement que la postconditon soit fausse ?
  5. Appel C

    >>> a = sur100(1) >>> a 100
  6. Sur l'appel D, qui est responsable du déclenchement de l'erreur ? Le concepteur ou l'utilisateur ?
  7. Appel D

    >>> a = sur100("0.8") ValueError: invalid literal for int() with base 10: '0.80.80.80.80.80.80.80.80.80.80.80.80.80.80.80.80.80.80.80.80.80.80.80.80.80.80.80.80.80.80.80.80.80.80.80.80.80.80.80.80.80.80.80.80.80.80.80.80.80.80.80.80.80.80.80.80.80.80.80.80.80.80.80.80.80.80
  8. Sur l'appel E, l'argument ne respecte pas les préconditions puisqu'on envoie un string. Cela déclenche-t-il une erreur qui stoppe l'exécution du programme ?
  9. Appel E

    >>> a = sur100("1") >>> a 1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111
  10. Quel est le risque (humain ou financier) si on utilise le résultat précédent pour faire un autre calcul ?

...CORRECTION...

  1. Un utilisateur peut très bien ne pas respecter les préconditions affichées dans la documentation. C'est son droit mais également sa responsabilité qui est engagée.
  2. On obtient un résultat supérieur à 100 à cause de l'utilisateur : la précondition est FAUSSE. Rien ne permet donc de garantir la postcondition.
  3. Appel B

    >>> a = sur100(1.12) >>> a 112
  4. Cette fois, on tombe sur un cas où la précondition est FAUSSE (l'utilisateur envoie un entier et pas un flottant comme demandé) et pourtant le résultat est correct. Rien ne garantit la postcondition dans ce cas, cela ne veut pas dire qu'elle est forcément fausse si la précondition est fausse.
  5. Appel C

    >>> a = sur100(1) >>> a 100
  6. L'utilisateur ne respecte pas la précondition d'envoyer un flottant et envoie un string. C'est le responsabilité de l'utilisateur qui est engagé.
  7. Appel D

    >>> a = sur100("0.8") ValueError: invalid literal for int() with base 10: '0.80.80.80.80.80.80.80.80.80.80.80.80.80.80.80.80.80.80.80.80.80.80.80.80.80.80.80.80.80.80.80.80.80.80.80.80.80.80.80.80.80.80.80.80.80.80.80.80.80.80.80.80.80.80.80.80.80.80.80.80.80.80.80.80.80.80
  8. Dernier cas : on ne respecte pas la précondition (on envoie un string) et pourtant la réponse est bonne. Ne pas respecter les préconditions ne déclenchent donc pas nécessairement de problème immédiat.
  9. Appel E

    >>> a = sur100("1")
  10. Le problème vient de faire que la fonction renvoie une réponse totalement fausse mais sans déclencher d'erreur. Utiliser cette réponse pour faire un autre calcul va donc engendrer d'autres réponses fausses et, potentiellement, créer un problème soit dans l'exécution du programme, soit dans le monde réel.

Nous avons donc vu la partie RESPONSABILITE DE L'UTILISATEUR.

  • Préconditions respectées IMPLIQUE Postconditions respectées.
  • Préconditions non respectées N'IMPLIQUE rien sur les postconditions. On navigue à vue.

Regardons maintenant le partage de la responsabilité entre le concepteur et son chef de projet.

03° Répondre aux questions suivantes après avoir relu la documentation de la fonction.

1 2 3 4 5 6 7 8 9 10 11 12 13
def sur100(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 :: exemples .. >>> sur100(0.5) 50 >>> sur100(0.05) 5 ''' return int(pourcentage*100)
  1. Trouve-t-on des exemples dans la documentation permettant de savoir comment on s'attend à ce que l'arrondi se comporte ? Arrondi inférieur ou arrondi supérieur ?
  2. Quel est le choix opéré par le concepteur de la fonction vis à vis des arrondis ?
>>> a = sur100(0.426) >>> a 42 >>> a = sur100(0.425) >>> a 42 >>> a = sur100(0.424) >>> a 42 >>> a = sur100(0.009999) >>> a 0 >>> a = sur100(0.99999999999999999999) >>> a 99

...CORRECTION...

On ne trouve rien dans la documentation permettant de savoir si on devrait arrondir au supérieur ou à l'inférieur.

Le concepteur a décidé de faire le choix d'arrondir à l'inférieur.

Tout pourcentage strictement inférieur à 0,01 donne donc 0%.

04° Voici ce que le chef de projet avait fourni initialement au concepteur pour lui expliquer le travail attendu :

1 2 3 4 5 6 7 8 9 10 11 12 13
def sur100(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 :: exemples .. >>> sur100(0.5) 50 >>> sur100(0.05) 5 ''' return 0

Notez bien que le chef n'a pas réalisé la fonction. Il a simplement écrit la documentation de façon à donner un cadre clair à celui qui va devoir transformer cette demande en instructions Python valides.

Le chef de projet a juste fourni une fonction qui répond bien par un integer mais c'est 0 tout le temps pour le moment.

Question : la fonction fournie par le chef de projet répond-t-elle correctement à la documentation fournie par le chef de projet ?

...CORRECTION...

On ne trouve rien dans la documentation permettant de savoir si on devrait arrondir au supérieur ou à l'inférieur.

Le concepteur a décidé de faire le choix d'arrondir à l'inférieur.

Tout pourcentage strictement inférieur à 0,01 donne donc 0%.

05° Le concepteur rend son travail (la ligne 5) en fournissant la fonction telle qu'on l'utilise depuis le début :

1 2 3 4 5 6 7 8 9 10 11 12 13
def sur100(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 :: exemples .. >>> sur100(0.5) 50 >>> sur100(0.05) 5 ''' return int(pourcentage*100)

Cette fonction arrondi donc à l'inférieur.

Question : le chef est furieux et critique le travail fourni. A-t-il raison de critiquer son subalterne ?

...CORRECTION...

Le chef de projet a tord : rien n'indique comment arrondir. En ne fournissant pas d'indication à ce propos, il indique au concepteur que cette question n'est pas importante. Le concepteur a donc juste fait un choix dans le cadre restreint qu'on lui a donné.

Bien entendu, il aurait été préférable ici qu'il demande des précisions au chef de projet. Mais après tout, qui est payé pour concevoir le projet et expliquer clairement les contraintes ?

06° Voici ce que le chef de projet aurait pu fournir au concepteur pour lui expliquer le travail attendu :

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
def sur100(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 :: exemples .. >>> sur100(0.004) 0 >>> sur100(0.005) 1 >>> sur100(0.05) 5 >>> sur100(0.5) 50 >>> sur100(0.994) 99 >>> sur100(0.995) 100 ''' return 0

Question : la fonction du concepteur avec arrondi inférieur répond-t-elle à la cette documentation comportant plus d'exemples fournie par le chef de projet ? Le chef a-t-il raison de râler lorsque sur100(0.009) renvoie 0 ?

...CORRECTION...

Cette fois, le concepteur a mal fait son travail : l'instruction qu'il a utilisé pour réaliser la fonction ne répond pas aux attentes.

>>> sur100(0.005) 1 >>> sur100(0.004) 0

Ces deux exemples permettent de comprendre qu'on travaille avec les arrondis classiques.

C'est donc le concepteur qui est en faute.

Malgré tout, le chef du projet aurait pu marquer qu'on renvoie un nombre arrondi de façon classique. Ca ne coûte rien et au moins c'est explicite. L'exemple se suffit en lui-même mais donne l'information de façon implicite.

Arrondir avec Python
  1. Fonction native int()
  2. Permet juste de récupérer la partie entière. Il ne s'agit donc pas réellement d'un arrondi au sens strict.

    >>> int(1.01) 1 >>> int(1.49) 1 >>> int(1.5) 1 >>> int(1.51) 1 >>> int(1.99) 1 >>> int(-4.99) -4
  3. Fonction native round()
  4. Comme son nom l'indique, cette fonction native permet d'arrondir un nombre, de façon classique donc.

    >>> round(1.01) 1 >>> round(1.49) 1 >>> round(1.5) 2 >>> round(1.51) 2 >>> round(1.99) 2 >>> round(-4.99) -5

    Attention néanmoins. S'agissant de flottant, la gestion de l'arrondi n'est pas toujours bien réalisé sur certains cas limites (notamment lorsqu'on se trouve entre 0 et 1) comme :

    >>> round(0.5) 0

    Et oui. 0 alors qu'on attend 1 puisqu'on arrondit au supérieur lorsqu'on a un 5.

    Par contre, cela marche correctement pour 0.49 et 0.51.

    >>> round(0.49) 0 >>> round(0.51) 1

    L'un des gros intérêts de la fonction native round() est qu'elle sait arrondir à un, deux... chiffres après la virgule.

    >>> round(0.4918,2) 0.49 >>> round(0.4978, 2) 0.5
  5. Fonction floor() du module math
  6. Voir la partie FAQ si vous voulez arrondir à l'inférieur.

  7. Fonction ceil() du module math
  8. Voir la partie FAQ si vous voulez arrondir au supérieur.

07° Voici la nouvelle version de la fonction sur100() produite pour qu'elle fonctionne exactement comme la documentation l'indique.

Cette fois, la demande par le chef de projet est vraiment claire : il indique clairement sur la ligne 6 de la documentation qu'il désire un arrondi classique et fourni en plus un ensemble d'exemples permettant de vérifier que la fonction renvoie ce qu'il veut.

Le concepteur de la fonction a donc dû réalisé une suite d'instructions plus compliquée mais qui fonctionne normalement.

Actions à réaliser : mettre la fonction en mémoire et vérifier un par un les exemples pour voir si le concepteur a bien fait son travail.

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 sur100(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 .. remarque : calculé avec l'arrondi classique (0.5 donne 1 et 0.49 donne 0) :: exemples .. >>> sur100(0.004) 0 >>> sur100(0.005) 1 >>> sur100(0.05) 5 >>> sur100(0.5) 50 >>> sur100(0.994) 99 >>> sur100(0.995) 100 ''' p = pourcentage * 100 if p != 0.5: resultat = round(p) else: resultat = 1 return resultat

...CORRECTION...

>>> sur100(0.004) 0 >>> sur100(0.005) 1 >>> sur100(0.05) 5 >>> sur100(0.99) 99 >>> sur100(0.999) 100

Tous ces tests répondent bien comme la documentation le demandent.

Et voilà pour la communication tout au long des différentes phases de la fonction.

  1. Le chef de projet réalise le prototype, la documentation en fournissant un ensemble d'exemples de fonctionnement avant même que la fonction ne soit effectivement codée.
  2. Le concepteur de la fonction se base sur cette documentation pour réaliser le travail demandé. Il doit se baser sur les précondtions, postconditions et les exemples pour savoir s'il a terminé son travail, ou pas.
  3. L'utilisateur n'a qu'à lire les préconditions et peut ainsi considérer que la réponse répond bien aux postconditions indiquées dans la documentation.

08° Réaliser un grand nombre d'exemples permet-il de garantir que la fonction va toujours répondre comme il le faudrait ?

...CORRECTION...

Non !

Les exemples ne sont que cela : des exemples.

Ils ne servent qu'à définir que la fonction fonctionne sur les quelques exemples fournis.

C'est exactement comme en mathématiques : montrer sur un exemple numérique que cela fonctionne ne permet pas de faire la généralisation du bon fonctionnement.

Pour montrer que cela fonctionne toujours, il faut faire la preuve de correction de votre fonction ou de votre algorithme. Voir la partie ALGORITHMIQUE.

2 - Réaliser des fonctions de test

Vous avez vu que tester l'intégralité d'un jeu de tests est long. On peut faire mieux : on peut réaliser une fonction qui va tester que la fonction répond correctement. Automatiquement.

Pour cela, nous allons utiliser un nouveau mot-clé : assert.

Vocabulaire

Proposition mathématique

Une proposition mathématique est un énoncé qui peut être soit vrai, soit faux.

Son équivalent en programmation est la notion d'expression booléenne qui peut être évaluée à True ou False.

Propriété

C'est une proposition toujours vraie associée à un objet mathématique.

Exemple : P = "Un carré possède 4 côtés égaux" est une propriété que partage tous les carrés. Cette propriété est bien toujours vraie lorsqu'elle s'applique à un carré.

Assertion

Voilà un mot intéressant car son sens réel dépend du contexte !

  • En philosophie, une assertion est une propriété qu'on présente ou déclare comme vraie.
  • En mathématiques, une assertion est une propriété mathématique vraie dans le cadre d'une théorie mathématique clairement définie.
  • En informatique, une assertion est une expression booléenne qui doit être True dans le cadre d'un fonctionnement normal. Si l'assertion est évaluée à False, il faut donc interrompre le fonctionnement normal du programme.

Comme vous le voyez, une assertion désigne donc quelque chose qui devrait être vraie mais qui peut s'avérer fausse parfois.

Assertion avec assert en Python

On peut créer des assertions avec le mot-clé assert en Python.

Son fonctionnement est simple :

  1. L'interpréteur Python évalue l'expression située derrière le mot-clé assert
  2. Si il obtient True, il continue en passant juste à la ligne suivante
  3. Si il obtient False, il lève une exception, ce qui a tendance par défaut à interrompre le déroulement du programme.

Exemple :

99 100 101
... une instruction ... assert note >= 0 ... une autre instruction ...

Lorsqu'il arrive L100, Python évalue l'expression note >= 0.

  • Si la note vaut 15 par exemple, l'expression est évaluée à True puisque 15 est supérieur à 0. On passe juste à la ligne 101.
  • Si la note vaut -2 par exemple, l'expression est évaluée à False puisque -2 n'est pas supérieur à 0. L'interpréteur Python lève une exception qui stoppe le programme.
09° Utiliser ce programme : vous devriez vérifier que l'appel de la fonction verifier_note() ne provoque aucune erreur puisque toutes les assertions sont bien évaluées à True (facile aucun des arguments n'est problématique).

1 2 3 4 5 6 7 8
def verifier_note(n): '''Vérifie que n est bien dans [0,20] et est un entier ou un flottant''' assert type(n) == int or type(n) == float assert n >= 0 assert n <= 20 verifier_note(15) verifier_note(12)
10° Utiliser ce programme : vous devriez vérifier que l'appel de la fonction verifier_note() ne provoque aucune erreur sur le premier appel mais provoque un arrêt du programme sur le second.

Questions

  • Comment se nomme l'erreur provoquée ?
  • Peut-on trouver facilement la ligne ayant déclenché l'erreur ?
1 2 3 4 5 6 7 8
def verifier_note(n): '''Vérifie que n est bien dans [0,20] et est un entier ou un flottant''' assert type(n) == int or type(n) == float assert n >= 0 assert n <= 20 verifier_note(15) verifier_note(22)

...CORRECTION...

File "/...nom_du_programme.py", line 5, in verifier_note assert n <= 20 AssertionError
  • Python indique une AssertionError.
  • On voit qu'il s'agit de la ligne 5. Celle de l'assertion vérifiant que la note est bien inférieure à 20.

Nous sommes donc parvenus à détecter automatiquement une "erreur" et faire stopper le programme plutôt que de laisser l'erreur corrompre d'autres parties du programme.

Mais, il n'est pas facile de voir d'où vient l'erreur puisqu'il faut aller voir la ligne 5 correspondante.

On peut faire mieux, en indiquant un message à transmettre en cas d'assertion déclenchant une interruption.

11° Utiliser ce programme : vous devriez vérifier que l'appel de la fonction verifier_note() ne provoque aucune erreur sur le premier appel mais provoque un arrêt du programme sur le second.

Question

  • Pourquoi est-il plus facile de comprendre ce qui se passe ?
1 2 3 4 5 6 7 8
def verifier_note(n): '''Vérifie que n est bien dans [0,20] et est un entier ou un flottant''' assert type(n) == int or type(n) == float assert n >= 0, "La note fournie est négative !" assert n <= 20, "La note fournie est supérieure à 20 !" verifier_note(15) verifier_note(22)

...CORRECTION...

File "/...nom_du_programme.py", line 5, in verifier_note assert n <= 20, "La note fournie est supérieure à 20 !" AssertionError: La note fournie est supérieure à 20 !

Le fait d'avoir fourni un string décrivant le problème permet de mieux comprendre d'où vient le problème.

Nous allons donc pouvoir utiliser cette possibilité de réaliser des assertions pour tester automatiquement les fonctions.

Nous allons réaliser des jeux de tests : plutôt que de simplement mettre les exemples simplement en tant que documentation, nous allons vérifier qu'ils correspondent bien à ce que la fonction réalise.

12° Utiliser ce programme. Il comporte :

  • Lignes 03-29 : la déclaration de la fonction sur100()
  • Lignes 31-37 : la déclaration de la fonction tester_sur100() qui est juste un jeu de tests : des assertions qui vérifie que lorsqu'on lance la fonction sur100(), on obtient bien la réponse attendue.
  • Lignes 41 : la seule instruction du programme : l'appel de la fonction tester_sur100().

Question : pourquoi sait-on après exécution que la fonction se comporte bien comme on le veut ?

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
# Déclaration des fonctions def sur100(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 .. remarque : calculé avec l'arrondi classique (0.5 donne 1 et 0.49 donne 0) :: exemples .. >>> sur100(0.004) 0 >>> sur100(0.005) 1 >>> sur100(0.05) 5 >>> sur100(0.5) 50 >>> sur100(0.994) 99 >>> sur100(0.995) 100 ''' p = pourcentage * 100 if p != 0.5: resultat = round(p) else: resultat = 1 return resultat def tester_sur100(): assert sur100(0.004) == 0 assert sur100(0.005) == 1 assert sur100(0.05) == 5 assert sur100(0.5) == 50 assert sur100(0.994) == 99 assert sur100(0.995) == 100 # Programme principal tester_sur100()

...CORRECTION...

On sait que la fonction fonctionne en respectant les exemples fournis puisqu'aucune exception n'a été levée.

13° Quelqu'un décide de modifier la fonction. Il décide de remplacer l'utiliser la fonction int() plutôt que round().

  1. Modifier la ligne 26 de façon à remplacer round() par int().
  2. 26
    resultat = int(p)
  3. Lancer le programme. Voit-on immédiatement que nous avons fait une modification malvenue ?

...CORRECTION...

On voit immédiatement que la modification effectuée n'était pas une bonne idée puisque notre fonction modifiée ne passe plus le jeu de tests.

14° Lancer ce programme. Vous devriez constater que pour le moment la fonction ne répond pas correctement. C'est normal, on lui demande de répondre toujours False.

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

Répondre aux tests veut-il dire que la fonction est nécessairement 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
# Déclaration des fonctions def est_dangereux(composition, allergene): '''Renvoie True si l'allergene est dans la composition, False sinon :: param composition(str) :: un string caractérisant la composition du produit :: param allergene(str) :: un string caractérisant l'allergène :: return (bool) :: True si le produit est dangereux >>> est_dangereux("sucre;blé;soja", "soja") True >>> est_dangereux("sucre;blé;soja", "sojja") False ''' return False def tester_est_dangereux(): assert est_dangereux("sucre;blé;soja", "soja") == True assert est_dangereux("sucre;blé;soja", "sojja") == False # Programme principal tester_est_dangereux()

...CORRECTION...

Il suffit de changer la ligne 16 :

return allergene in composition

Une fonction passant les tests ne veut pas dire que la fonction est correcte. Cela veut juste dire que la fonction passe les tests.

Notez bien qu'ici je me donne le droit d'écrire des assertions avec == False ou == True car le but est d'être explicite : on cherche à vérifier que la fonction réponde bien cela.

19 20
assert est_dangereux("sucre;blé;soja", "soja") == True assert est_dangereux("sucre;blé;soja", "sojja") == False

Par contre, si vous devez écrire des instructions conditionnelles, préférez bien évidemment ceci :

1 2 3 4 5
if est_dangereux("sucre;blé;soja", "soja"): instructions à faire si la fonction a répondu True if not est_dangereux("sucre;blé;soja", "sojja"): instructions à faire si la fonction a répondu False

La documentation dans le cadre de la vie d'une fonction :

Nous avons donc vu :

  • Que le chef d'un projet peut fournir à des programmeurs des tâches à réaliser sous forme :
    1. du prototype de la fonction voulue accompagnée de sa documentation (préconditions, postconditions et exemples)
    2. d'une fonction de test comportant un ensemble de tests bien choisis que la fonction devra passer une fois réalisée (et c'est un travail compliqué de savoir quels tests demandés en réalité)
  • Que le programmeur-concepteur de la fonction peut alors implémenter la demande en réalisant la fonction et il peut la tester à l'aide du jeu de tests fournis.
  • Qu'un utilisateur de la fonction peut utiliser la fonction simplement en récupérant la documentation

Où se trouve le travail du développeur là-dedans ? Un peu dans les 3 trois domaines, en fonction du degré de compétences de la personne, de la taille de l'équipe.

Si vous voulez en savoir plus sur, une partie, des métiers de l'informatique : ONISEP.

Conclusion finale sur les tests : comprennez bien 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 et encore moins de prouver la correction d'une fonction ou d'un algorithme.

3 - Doctest

Nous allons voir maintenant que Python nous permet d'aller plus vite dans la conception de nos fonctions. Pour l'instant, on doit faire presque le même travail de fois :

  1. On place des exemples dans la documentation
  2. On réalise une fonction de test contenant des assert qui vérifie que la fonction répond aux attentes sur quelques exemples

Dans cette partie, nous allons gagner du temps : nous allons utiliser le module doctest qui permet d'utiliser les exemples de la documentation pour réaliser les tests ! Plus besoin de faire le travail deux fois.

Module doctest

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

import doctest

Le mot derrière import est doctest, on doit donc utiliser ce terme pour accéder aux fonctions contenues dans le module.Pour activer l'utilisation automatique des exemples de documentations en tant que tests, il suffit d'utiliser la fonction testmod() du module doctest.

On trouve souvent un appel en fin de programme, formulé de cette façon dans les pages Web qui en parlent :

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

Cette instruction conditionnelle if __name__ == "__main__" va permettre de lancer la fonction testmod() uniquement si vous venez de lancer directement le programme.

15° Utiliser le code suivant qui ne contient que la fonction (fausse, voir ligne 26) et l'activation du doctest.

  1. Observer s'il se passe quelque chose.
  2. Où se trouve le test qui permet d'invalider la fonction pour l'instant ?
  3. Identifier le problème et modifier 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
# Déclaration des fonctions def note_valide(note): '''Renvoie True si note est dans l'intervalle [ 0 ; 20 ], False sinon : param note (int|float): la note à tester : return (bool) : True si note dans [ 0 ; 20 ], sinon False :Exemple: >>> note_valide(5) True >>> note_valide(-5) False >>> note_valide(25) False >>> note_valide(20.0) True ''' return note >= 0 and note <= 30 # 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 "mon_fichier_python.py", line 14, 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.

En allant voir la ligne 14, on voit bien qu'il s'agit d'un problème lié à 25 : 25 est validé à True.

Il suffit d'aller voir le return pour comprendre.

20
return note >= 0 and note <= 30

A transformer en :

20
return note >= 0 and note <= 20

Lorsqu'on repasse les tests après avoir correctement modifié la ligne 20, on constate qu'il ne se passe plus rien. C'est l'effet attendu : à partir du moment où la fonction passe les tests, on ne signale rien.

Erreurs typiques avec doctest

Erreur typique n°1 : oublier de laisser une ligne vide après le dernier test. Ici, c'est la ligne 18. Sans cette ligne, le module risque parfois de ne pas parvenir à détecter la fin du jeu de test et penser que les trois guillemets font partie de la réponse attendue.

Erreur typique n°2 : la présence de quelques caractères espaces derrière une réponse : la fonction testmod() teste l'égalité parfaite entre la réponse de la fonction et la réponse de la documentation. Si vous placez des caractères Espace dans la documentation, il va vous dire qu'il n'y a pas concordance entre réponse réelle et exemple.

16° Placer ce nouveau programme en mémoire. Il ressemble comme deux gouttes d'eau au précédent. Sauf qu'il y a deux espaces en trop sur l'un des tests.

Visualiser que cela déclenche un faux-positif puis chercher la ligne d'exemples qui pose problème.

Corriger pour vous convaincre que cela vient bien de là.

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
# Déclaration des fonctions def note_valide(note): '''Renvoie True si note est dans l'intervalle [ 0 ; 20 ], False sinon : param note (int|float): la note à tester : return (bool) : True si note dans [ 0 ; 20 ], sinon False :Exemple: >>> note_valide(5) True >>> note_valide(-5) False >>> note_valide(25) False >>> note_valide(20.0) True ''' return note >= 0 and note <= 20 # Corps du programme if __name__ == "__main__": import doctest doctest.testmod()
********************************************************************** File "mon_fichier_python.py", line 12, in __main__.note_valide Failed example: note_valide(-5) Expected: False Got: False ********************************************************************** 1 items had failures: 1 of 4 in __main__.note_valide ***Test Failed*** 1 failures.

...CORRECTION...

La méthode la plus efficace pour trouver un problème d'espaces est de sélectionner les exemples avec la souris. Vous allez nécessairement voir qu'il y a des espaces après le False de la ligne 13 des exemples.

12 13
>>> note_valide(-5) False

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

17° Utiliser ce nouveau programme, mais dans lequel on demande en ligne 26 à la fonction testmod() du module doctest beaucoup de "verbiage". On lui demande de dire si les tests se sont bien passés.

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
# Déclaration des fonctions def note_valide(note): '''Renvoie True si note est dans l'intervalle [ 0 ; 20 ], False sinon : param note (int|float): la note à tester : return (bool) : True si note dans [ 0 ; 20 ], sinon False :Exemple: >>> note_valide(5) True >>> note_valide(-5) False >>> note_valide(25) False >>> note_valide(20.0) True ''' return note >= 0 and note <= 20 # Corps du programme if __name__ == "__main__": import doctest doctest.testmod(verbose=True)

Pratique si on veut avoir un bilan des tests qui ont été validés par la fonction actuelle.

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.

Les jeux de tests permettent donc de tester la validité des fonctions la première fois qu'on les réalise mais également de visualiser les bugs éventuels lorsqu'on les modifie par la suite. Détecter automatiquement que la modification qu'on vient de faire crée un problème est un réel avantage. Mais qui a inventé le terme "bug" ?

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

4 - Morpion

Deux petits exos qui reprennent une grande partie des connaissances vues aujourd'hui.

On vous demande de créer des fonctions. Sont fournies directement dans la documentation :

  • Les explications rapides
  • Les préconditions
  • Les postconditions

Nous allons donc travailler sur un prototype de morpion. Le programme est non finalisé, notamment car il n'utilise pas de tableaux. Cela rend le tout plus difficile à concevoir de façon harmonieuse.

Comme de nombreux profs demandent des projets Morpion, j'ai décidé ici de fournir un Morpion très mal programmé : utilisation de variables globales, pas de tableaux... Ne l'utilisez pas pour faire croire à votre prof qui vous avez réalisé cela. Vous risquez d'obtenir une très mauvaise note !

18° Utiliser le programme qui se trouve sous cette question pour vérifier qu'il fonctionne. S'il ne fonctionne pas, penser à vérifier les modules dans Thonny ou Python. Le mini-projet ne demande aucune connaissance en Tkinter : vous n'aurez qu'à modifier des fonctions "gestion de données". Toute la partie Interface Graphique est fournie.

Nous pouvons voir que :

  • Lignes 199 à 224, on crée les widgets de classe Label en leur donnant une couleur particulière. On utilise pour cela la fonction creer_carre().
  • 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224
    carre1 = creer_carre("black","yellow") carre1.place(x=50, y=25) carre2 = creer_carre("black","yellow") carre2.place(x=150, y=25) carre3 = creer_carre("black","yellow") carre3.place(x=250, y=25) carre4 = creer_carre("black","yellow") carre4.place(x=50, y=125) carre5 = creer_carre("black","yellow") carre5.place(x=150, y=125) carre6 = creer_carre("black","yellow") carre6.place(x=250, y=125) carre7 = creer_carre("black","yellow") carre7.place(x=50, y=225) carre8 = creer_carre("black","yellow") carre8.place(x=150, y=225) carre9 = creer_carre("black","yellow") carre9.place(x=250, y=225)
  • Les widgets sont numérotés dans l'ordre 
    • 1er ligne : 1 - 2 - 3
    • 2e ligne :  4 - 5 - 6
    • 3e ligne :  7 - 8 - 9
  • On crée un évenement à surveiller ligne 229 : lorsqu'on clique sur le premier bouton de la souris, cela active la fonction choix_case().
  • 229
    fen_princ.bind('<Button-1>',choix_case)
  • ligne 230 : On lance la surveillance en boucle de la fenêtre avec la méthode mainloop().
  • 230
    fen_princ.mainloop()

Ne cherchez pas à rentrer dans les détails, survoler simplement vaguement le code pour l'instant.

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 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230
'''Attention : Code non fonctionnel dans le cadre d'un projet. Ce code est volontairement non optimisé de façon à ne pas pouvoir être utilisé tel quel dans le cadre d'un projet. Il ne contient ni tableau, ni génération automatique des cases et utilise des variables globales qu'on modifient. On teste parfois les variables symbole1, parfois directement en notant 'O'... Bref, ne recopier pas ceci pour vos projets si vous voulez une bonne note :o) ''' # - - - - - - - - - # 1 - importation # - - - - - - - - - import tkinter as tk # - - - - - - - - - - - - - - - - # 2 - Var. globales et constantes # - - - - - - - - - - - - - - - - - symbole1 = "O" # symbole affiché pour le joueur 1 couleur1 = "red" # couleur pour le joueur 1 symbole2 = "X" # symbole affiché pour le joueur 2 couleur2 = "blue" # couleur pour le joueur 2 joueur = symbole1 # Variable contenant le symbole du joueur actif couleur = couleur1 # Variable contenant la couleur du joueur actif message = "Jeu en cours" vide = "?" # Symbole d'une case encore vide # - - - - - - - - - - - - - - - - - - - # 3A - Fonctions Interface Graphique- - # - - - - - - - - - - - - - - - - - - - def initialisation(): '''Procédure qui initialise les différents widgets et variables''' global joueur global couleur joueur = symbole1 couleur = couleur1 carre1.configure(text='?', bg="#777777") carre2.configure(text='?', bg="#777777") carre3.configure(text='?', bg="#777777") carre4.configure(text='?', bg="#777777") carre5.configure(text='?', bg="#777777") carre6.configure(text='?', bg="#777777") carre7.configure(text='?', bg="#777777") carre8.configure(text='?', bg="#777777") carre9.configure(text='?', bg="#777777") communication.configure(text=message) def creer_carre(couleur_fond, couleur_texte): '''Fonction qui génére un widget carré coloré et renvoie sa référence ::param couleur_fond(str) :: une couleur de fond valide pour Tk ::param couleur_texte(str) :: une couleur d'écriture valide pour Tk ::return (tk.Label) :: la référence du widget créé .. note:: on peut fournir les couleurs sous forme 'red' ou '#FF0000' ''' zone = tk.Label(fen_princ, text = "?", fg=couleur_texte, bg=couleur_fond, width=10, height=5) return zone def choix_case(event): '''Procédure évenementielle principale du jeu ::param event (Event) :: l'événement ayant provoqué l'activation de la fonction ''' # On regarde le messsage dans communication pour savoir si on relance une partie ou non if communication.cget("text") == message: w_case = event.widget # On récupère la référence du widget sur lequel on vient de cliquer if isinstance(w_case,tk.Label) == True: # Si le joueur n'a pas cliqué sur le fond de la fenêtre if possible(w_case.cget('text')): changer_case(w_case) changer_joueur() # On récupère le str affiché dans les 9 Labels c1 = carre1.cget('text') c2 = carre2.cget('text') c3 = carre3.cget('text') c4 = carre4.cget('text') c5 = carre5.cget('text') c6 = carre6.cget('text') c7 = carre7.cget('text') c8 = carre8.cget('text') c9 = carre9.cget('text') fin = fin_de_jeu(c1,c2,c3,c4,c5,c6,c7,c8,c9) if fin != 0: gagnant = f"Joueur {fin} gagne ! Appuyer pour recommencer" communication.configure(text=gagnant) else: # On a cliqué alors que le jeu était fini : il faut relancer initialisation() def changer_case(w_case): '''Procédure qui modifie le contenu et la couleur de la case''' w_case.configure(bg=couleur) w_case.configure(text=joueur) # - - - - - - - - - - - - - - - - - - - # 3B - Fonctions Gestion de données - - # - - - - - - - - - - - - - - - - - - - def changer_joueur(): '''Procédure qui inverse les propriétés symbole et couleur du joueur en cours''' global joueur global couleur if joueur == symbole1: joueur = symbole2 couleur = couleur2 else: joueur = symbole1 couleur = couleur1 def verifier(chaine): '''Fonction qui vérifie si chaine peut correspondre à une victoire d'un des joueurs ::param chaine (str) :: un string concaténant le contenu d'une configuration gagnante ::return (int) :: le numéro du gagnant (1 si 'OOO' ou 2 si 'XXX'), 0 sinon >>> verifier("OOO") 1 >>> verifier("XXX") 2 >>> verifier("X?O") 0 ''' return 0 def fin_de_jeu(c1,c2,c3,c4,c5,c6,c7,c8,c9): '''Fonction qui renvoie le numéro du joueur qui a gagné, 0 sinon ::param c1 à c9 (str) :: contenu de la case (normalement '?', 'O' pour joueur 1 ou 'X' pour joueur 2) ::return (int) :: renvoie 0 si personne n'a gagné, 1 ou 2 si un joueur gagne et -1 si personne ne peut gagner >>> fin_de_jeu('?','?','?','?','?','?','?','?','?') 0 >>> fin_de_jeu('O','O','O','?','?','?','?','?','?') 1 >>> fin_de_jeu('X','O','O','X','?','?','X','?','?') 2 >>> fin_de_jeu('O','X','O','?','O','?','?','?','O') 1 ''' gagnant = 0 # Vérification des différents cas gagnants en ligne if gagnant == 0: gagnant = verifier(c1+c2+c3) if gagnant == 0: gagnant = verifier(c4+c5+c6) if gagnant == 0: # c'est bien un nouveau if gagnant = verifier(c7+c8+c9) # Vérification des différents cas gagnants en colonne # ... à compléter ... # Vérification des différents cas gagnants en diagonale # ... à compléter ... # Si gagnant vaut encore 0 ici, c'est qu'on a pas de gagnant if gagnant == 0: total = c1+c2+c3+c4+c5+c6+c7+c8+c9 if not "?" in total: # il ne reste plus de case vide ? gagnant = -1 return gagnant def possible(contenu_case): '''Prédicat qui renvoie True si le contenu de la case n'est pas encore choisi ::param contenu_case(str) :: le contenu de la case à surveiller ::return (bool) :: True uniquement si le contenu vaut '?' >>> possible('?') True >>> possible('') True >>> possible('O') False >>> possible('X') False ''' return True # - # Corps du programme # - if __name__ == "__main__": import doctest doctest.testmod() fen_princ = tk.Tk() fen_princ.geometry("600x600") carre1 = creer_carre("black","yellow") carre1.place(x=50, y=25) carre2 = creer_carre("black","yellow") carre2.place(x=150, y=25) carre3 = creer_carre("black","yellow") carre3.place(x=250, y=25) carre4 = creer_carre("black","yellow") carre4.place(x=50, y=125) carre5 = creer_carre("black","yellow") carre5.place(x=150, y=125) carre6 = creer_carre("black","yellow") carre6.place(x=250, y=125) carre7 = creer_carre("black","yellow") carre7.place(x=50, y=225) carre8 = creer_carre("black","yellow") carre8.place(x=150, y=225) carre9 = creer_carre("black","yellow") carre9.place(x=250, y=225) communication = tk.Label(fen_princ, text=message, fg='grey', bg='black', width=40, height=5) communication.place(x=50, y=425) fen_princ.bind('<Button-1>',choix_case) fen_princ.mainloop()

En jouant un peu, vous devriez vous rendre compte que le jeu est loin d'être finalisé.

Commençons par la fonction verifier(). Notez qu'une solution naïve des questions 19-20-21 est disponible après la dernière question.

19° Compléter la fonction verifier() de façon à ce qu'elle remplisse correctement sa tâche : vous devrez comparer le paramètre chaine avec les cas gagnants pour renvoyer la bonne réponse à l'aide d'un return. Vous aurez donc besoin d'utiliser de tester les autres possibilités de victoires.

Utilisez la documentation et les exemples de la documentation pour obtenir plus d'explications sur les entrées et les sorties de votre fonction.

Tant que le module doctest vous signale un problème sur l'un des tests de cette fonction, c'est qu'il y a un problème. Pour les deux autres fonctions, c'est normal : elles ne sont pas encore finalisées.

20° Compléter la fonction fin_de_jeu() de façon à ce qu'elle remplisse correctement sa tâche : pour l'instant, on ne règle que le cas des solutions horizontales. Reste les pions verticaux ou en diagonale. Tant que les doctests sont faux, c'est que le code n'est pas bon !

21° Finaliser le jeu avec la fonction possible() : c'est elle qui dit si on a le droit de mettre son pion sur une case. Pour l'instant, on peut mettre son pion sur une case déjà sélectionnée par l'adverse 

...CORRECTION 19-20-21...

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
def verifier(chaine): '''Fonction qui vérifie si chaine peut correspondre à une victoire d'un des joueurs ::param chaine (str) :: un string concaténant le contenu d'une configuration gagnante ::return (int) :: le numéro du gagnant (1 si 'OOO' ou 2 si 'XXX'), 0 sinon >>> verifier("OOO") 1 >>> verifier("XXX") 2 >>> verifier("X?O") 0 ''' # Les chaînes gagnantes : concaténation des 3 symbôles du joueur j1_win = 3 * symbole1 j2_win = 3 * symbole2 # Vérification cas gagnant ? gagnant = 0 if chaine == j1_win: gagnant = 1 elif chaine == j2_win: gagnant = 2 return gagnant def fin_de_jeu(c1,c2,c3,c4,c5,c6,c7,c8,c9): '''Fonction qui renvoie le numéro du joueur qui a gagné, 0 ou -1 sinon ::param cX (str) :: contenu de la case (normalement '?', 'O' pour joueur 1 ou 'X' pour joueur 2) ::return (int) :: renvoie 0 si personne n'a gagné, 1 ou 2 si un joueur gagne et -1 si personne ne peut gagner >>> fin_de_jeu('?','?','?','?','?','?','?','?','?') 0 >>> fin_de_jeu('O','O','O','?','?','?','?','?','?') 1 >>> fin_de_jeu('X','O','O','X','?','?','X','?','?') 2 >>> fin_de_jeu('O','X','O','?','O','?','?','?','O') 1 ''' # Vérification des différents cas gagnants en ligne gagnant = verifier(c1+c2+c3) if gagnant == 0: gagnant = verifier(c4+c5+c6) if gagnant == 0: # c'est bien un nouveau if gagnant = verifier(c7+c8+c9) # Vérification naïve des différents cas gagnants en colonne if gagnant == 0: gagnant = verifier(c1+c4+c7) if gagnant == 0: gagnant = verifier(c2+c5+c8) if gagnant == 0: gagnant = verifier(c3+c6+c9) # Vérification naïve des différents cas gagnants en diagonale if gagnant == 0: gagnant = verifier(c1+c5+c9) if gagnant == 0: gagnant = verifier(c3+c5+c7) # Un tableau contenant les cas à traiter nous aurait permis d'avoir juste une boucle WHILE à écrire :o) # Si gagnant vaut encore 0 ici, c'est qu'on a pas de gagnant if gagnant == 0: total = c1+c2+c3+c4+c5+c6+c7+c8+c9 if not "?" in total: # il ne reste plus de case vide ? gagnant = -1 return gagnant def possible(contenu_case): '''Prédicat qui renvoie True si le contenu de la case n'est pas encore choisi ::param contenu_case(str) :: le contenu de la case à surveiller ::return (bool) :: True uniquement si le contenu vaut '?' >>> possible('?') True >>> possible(vide) True >>> possible(symbole1) False >>> possible(symbole2) False ''' return contenu_case == "?"

5 - FAQ

Les arrondis inférieur et supérieur du module math

  1. Fonction floor() du module math
  2. Comme son nom l'indique (floor voulant dire sol en anglais), cette fonction renvoie l'entier juste immédiatement inférieur ou égal au nombre envoyé.

    Première façon de faire : on place floor directement derrière le import.

    >>> from math import floor >>> floor(1.01) 1 >>> floor(1.49) 1 >>> floor(1.5) 1 >>> floor(1.51) 1 >>> floor(1.99) 1 >>> floor(-4.99) -5

    Deuxième façon de faire : on place juste math derrière import.

    >>> import math >>> math.floor(1.5) 1
  3. Fonction ceil() du module math
  4. Comme son nom l'indique (ceil voulant dire plafonds en anglais), cette fonction renvoie l'entier juste supérieur ou égal au nombre reçu.

    Première façon de faire : on place ceil directement derrière le import.

    >>> from math import ceil >>> ceil(1.01) 2 >>> ceil(1.49) 2 >>> ceil(1.5) 2 >>> ceil(1.51) 2 >>> ceil(1.99) 2 >>> floor(-4.99) -4

    Deuxième façon de faire : on place juste math derrière import.

    >>> import math >>> math.ceil(1.5) 2

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 un return), et procédure (sans return ou avec une réponse toujours à None)
  • les arguments et les paramètres des fonctions
  • la portée des variables interagissant avec les fonctions
  • la documentation
  • les jeu de tests (via une fonction à part avec assert ou en utilisant la fonction testmod() du module doctest) permettant de vérifier le bon fonctionnement de la fonction
  • la structuration d'un programme complexe en multiples petites fonctions

Activité publiée le 28 08 2019
Dernière modification : 03 03 2022
Auteur : ows. h.