19 - 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.
Nous allons voir aujourd'hui qu'on peut définir un contrat de confiance clair entre le concepteur d'une fonction et ses utilisateurs, de façon ensuite à 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....
Documents de cours PDF : .PDF ou .ODT
Logiciel nécessaire pour l'activité : Python 3
Evaluation ✎ : questions 07-
1 - Contrat de confiance : préconditions et postconditions
La programmation est, avant tout, une affaire de communication.
1-1 Communication programmeur humain - interpréteur Python
Commençons par voir ce qui se passe si le programmeur communique mal avec l'interpréteur Python.
Erreur de syntaxe
Les fonctions que vous écrivez en Python doivent respecter la syntaxe de Python de façon à ce que votre texte soit compréhensible par l'interpréteur Python qui ne peut pas s'adapter à un texte mal conçu.
Si vous ne suivez pas les règles, l'interpréteur ne vous comprendra pas et va vous le dire en signalant en rouge une erreur de syntaxe : votre demande ne respecte pas sa façon de communiquer.
Exemple 1
>>> print "bonjour"
File "<pyshell>", line 1
print "bonjour"
^
SyntaxError: Missing parentheses in call to 'print'. Did you mean print("bonjour")?
L'interpréteur Python est perdu car on a oublié de placer les parenthèses autour de l'argument envoyé.
Exemple 2
1
2
3
4
5 |
|
>>> %Run script_essai.py
def f(a,b)
^
SyntaxError: invalid syntax
L'interpréteur Python est perdu car on a oublié de placer le : final de la déclaration de la fonction.
1-2 Communication utilisateur humain - concepteur humain
Reste encore un problème majeur de communication : celle entre la personne qui a conçu une fonction et la personne qui veut juste l'utiliser.
Dans ce cas, la communication passe par la documentation de la fonction.
Documentation : pour savoir comment utiliser la fonction
Il faut documenter les fonctions : la documentation doit expliquer comment utiliser la fonction. Une sorte de mode d'emploi de votre fonction. Ces informations doivent être suffisantes pour utiliser la fonction sans provoquer d'erreur.
Les délimiteurs de la documentation sont 3 guillemets simples en Python.
La documentation doit contenir au minimum :
- Une courte phrase expliquant à quoi elle sert
- Les preconditions : il s'agit des contraintes sur les types et les valeurs possibles des paramètres.
- Les postconditions : il s'agit cette fois d'une description de la sortie renvoyée par la fonction
Exemple
1
2
3
4
5
6
7
8
9
10 | def moyenne(a, b):
"""Renvoie la moyenne des notes a et b
:: param a(int|float) : nombre dans l'intervalle [ 0 ; 20 ]
:: param b(int|float) : nombre dans l'intervalle [ 0 ; 20 ]
:: return (float): la moyenne des deux notes, dans [ 0.0 ; 20.0 ]
"""
m = (a + b) / 2
return m
|
Si on lit cette documentation :
- On peut lire en ligne 2 qu'elle calcule une moyenne
- Sur les lignes 5-, on indique qu'on doit lui envoyer deux notes sous forme d'un nombre compris entre 0 et 20, inclus.
- La ligne 6 permet de comprendre que la fonction va renvoyer un nombre compris entre 0 et 20 et que son type sera float.
2
| """Renvoie la moyenne des notes a et b
|
4
5 | :: param a(int|float) : nombre dans l'intervalle [ 0 ; 20 ]
:: param b(int|float) : nombre dans l'intervalle [ 0 ; 20 ]
|
La barre verticale | (ALT-GR+6) veut dire OU dans ce contexte. On voit donc que le nombre peut être transmis soit sous forme d'integer ou de flottant.
6 | :: return (float): la moyenne des deux notes, dans [ 0.0 ; 20.0 ]
|
Récupérer la documentation
Pour récupérer la documentation d'une fonction mise en mémoire, il suffit d'utiliser la fonction native help(). Pratique lorsque votre projet fait 300000 lignes.
>>> help(moyenne)
Help on function moyenne in module __main__:
moyenne(a, b)
Renvoie la moyenne des notes a et b
:: param a(int|float) : nombre dans l'intervalle [ 0 ; 20 ]
:: param b(int|float) : nombre dans l'intervalle [ 0 ; 20 ]
:: return (float): la moyenne des deux notes, dans [ 0.0 ; 20.0 ]
01° Placer le programme suivant dans Thonny. Lancer les appels proposés dans la console puis répondre aux questions ci-dessous.
1
2
3
4
5
6
7
8
9
10 | def moyenne(a, b):
"""Renvoie la moyenne des notes a et b
:: param a(int|float) : nombre dans l'intervalle [ 0 ; 20 ]
:: param b(int|float) : nombre dans l'intervalle [ 0 ; 20 ]
:: return (float): la moyenne des deux notes, dans [ 0.0 ; 20.0 ]
"""
m = (a + b) / 2
return m
|
>>> moyenne(0, 0)
0.0
>>> moyenne(0.0, 20.0)
10.0
>>> moyenne(14.0, 20)
17.0
>>> moyenne(14.0, 32)
23.0
- Sur quelle ligne se trouve la courte description de la fonction ?
- Est-il possible que la sortie ne soit pas un nombre compris entre 0 et 20 si les deux paramètres sont bien des nombres compris entre 0 et 20
- Est-il possible que la sortie ne soit pas un nombre compris entre 0 et 20 si l'un des deux paramètres ne respecte pas les préconditions
- L'utilisateur de la fonction est-il obligé de suivre les préconditions ?
...CORRECTION...
Réponse A
La courte phrase se trouve en ligne 2, juste sous la ligne du prototype.
Réponse B
Si les deux nombres sont bien compris entre 0 et 20, il est évident que la moyenne des deux ne pourra qu'être dans l'intervalle [0; 20] également.
>>> moyenne(0, 0)
0.0
>>> moyenne(0.0, 20.0)
10.0
>>> moyenne(14.0, 20)
17.0
Réponse C
Par contre, on voit avec le dernier exemple que si l'utilisateur n'a pas respecter les contraintes imposées sur les paramètres, la sortie ne respecte plus non plus les contraintes notées. On peut donc dire que si les préconditions ne sont pas respectées, l'utilisateur ne peut plus considérer les yeux fermés que la postcondition soit vraie non plus.
>>> moyenne(14.0, 32)
23.0
Réponse D
Puisque nous sommes parvenus à taper l'exemple précédent, c'est bien que rien n'impose d'utiliser la fonction n'importe comment !
Par contre, si on tape n'importe quoi en entrée, on en paye le prix : on ne peut plus se baser sur la postcondition ou cela peut même provoquer une erreur.
02° Quelqu'un tape ceci. L'erreur est-elle imputable à l'utilisateur de la fonction ou au concepteur de la fonction ?
Pour répondre, il suffit de se demander si l'utilisateur a bien respecté les préconditions, ou pas.
>>> moyenne("4", "20")
File "/home/rv/Documents/TESTS_PROG/test_python/activite.py", line 9, in moyenne
m = (a + b) / 2
TypeError: unsupported operand type(s) for /: 'str' and 'int'
...CORRECTION...
La documentation permet de trancher : les paramètres doivent être des integers ou des floats. Envoyer un string ne correspond donc pas aux préconditions. C'est donc bien la faute de l'utilisateur.
03° Voici une fonction permettant de jouer à pierre-feuille-ciseaux. Quelqu'un tape ceci. L'erreur est-elle imputable
- à l'utilisateur de la fonction (car il n'a pas respecter les préconditions visibles lors de ses appels à la fonction) ?
- au concepteur de la fonction (car il n'a pas bien réalisé la fonction et elle ne respecte pas la postcondition) ?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 |
|
>>> pfc("pierre", "feuille")
2
Ok , c'est bien le joueur 2 qui gagne avec sa "feuille" qui entoure la "pierre".
>>> pfc("feuille", "pierre")
2
La fonction déclare que c'est le joueur 2 qui gagne. Problème, c'est le joueur 1 qui gagne normalement !
>>> pfc("feuille", "feuille")
2
Nouveau problème, c'est une égalité normalement !
...CORRECTION...
Cette fois, on voit bien que l'utilisateur a totalement respecté les préconditions.
Or, la fonction ne répond pas correctement. Cette fois, c'est la faute du concepteur de la fonction.
Si vous cherchez d'où viennent les erreurs de conception, il y a déjà "pierre" écrit "piere", avec un seul r, ligne 14. L'erreur de l'égalité vient du fait que le concepteur dit lui même qu'on répond 1 ou 2, il n'a pas géré l'égalité. Sa copie est à revoir.
A COMPRENDRE (IMPORTANT) : préconditions et postconditions
Contrat de confiance
Les préconditions sont donc les conditions ou contraintes que l'utilisateur de la fonction doit respecter.
Il y a donc une sorte de contrat de confiance entre utilisateur et concepteur :
- L'utilisateur est le garant du respect des préconditions lors des appels
- Le concepteur garantit que la postcondition est alors vérifiée (sous condition du respect des préconditions donc).
Premier cas : l'utilisateur respecte les préconditions
Dans ce cas, l'utilisateur peut concevoir le reste de son programme en considérant que la postcondition est vraie : le concepteur de la fonction s’y est engagé.
Si la fonction donne une mauvaise réponse ou provoque une exception, c'est la faute du concepteur car l'utilisateur a envoyé des données respectant les préconditions.
Deuxième cas : l'utilisateur ne respecte pas les préconditions
Dans ce cas, on ne peut pas tenir compte de la postcondition.
La fonction peut
- fonctionner correctement par hasard ou
- donner une mauvaise réponse ou
- provoquer une erreur.
En tous cas, c'est clairement la faute de l'utilisateur : il a utilisé la fonction hors des clous.
2 - Tester manuellement
04° 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)
|
- Sur quelle ligne peut-on apprendre ce que réalise globalement cette fonction ?
- 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 ?
- En étudiant la postcondition visible L5, quel doit-être le type du résultat renvoyée ?
- Selon l'exemple des lignes 7-8, quel est le résultat attendu si on lance un appel avec l'argument 0.5 ?
- Réaliser les exemples fournis dans la documentation pour vérifier qu'ils correspondent à la réalité de ce que fait la fonction.
- Notez bien la présence d'une ligne vide (L11) après le dernier exemple et la suite de la documentation.
>>> sur100(0.5)
???
>>> sur100(0.05)
???
...CORRECTION...
- Il suffit de regarder sous la ligne du prototype. Ligne 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.
- En étudiant la postcondition visible L5, on voit que la fonction va renvoyer un entier.
- Il suffit de lire : 50
- On tape ceci :
>>> sur100(0.5)
50
>>> sur100(0.05)
5
Nous allons maintenant revenir sur cela :
05° Répondre aux questions (et lancer les appels éventuels pour voir le résultat) :
- Un utilisateur peut-il envoyer des arguments ne respectant pas les préconditions ?
- 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 ?
- 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 ?
- Sur l'appel D, qui est responsable du déclenchement de l'erreur ? Le concepteur ou l'utilisateur ?
- 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 ?
- Quel est le risque (humain ou financier) si on utilise le résultat précédent pour faire un autre calcul ?
Appel B
>>> a = sur100(1.12)
>>> a
112
Appel C
>>> a = sur100(1)
>>> a
100
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
Appel E
>>> a = sur100("1")
>>> a
1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111
...CORRECTION...
- 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.
- 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.
- 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.
- 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é.
- 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.
- 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.
Appel B
>>> a = sur100(1.12)
>>> a
112
Appel C
>>> a = sur100(1)
>>> a
100
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
Appel E
>>> a = sur100("1")
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.
06° 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)
|
- 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 ?
- 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%.
07° 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%.
08° 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 arrondit 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 ?
09° 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
- Fonction native int()
- Fonction native round()
- Fonction floor() du module math
- Fonction ceil() du module math
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
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
Voir la partie FAQ si vous voulez arrondir à l'inférieur.
Voir la partie FAQ si vous voulez arrondir au supérieur.
10° 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 |
|
...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.
- 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.
- 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.
- 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.
11° 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.
3 - 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 :
- L'interpréteur Python évalue l'expression située derrière le mot-clé assert
- Si il obtient True, il continue en passant juste à la ligne suivante
- 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 |
|
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.
1
2
3
4
5
6
7
8 |
|
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 |
|
...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.
Question
- Pourquoi est-il plus facile de comprendre ce qui se passe ?
1
2
3
4
5
6
7
8 |
|
...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.
15° 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 |
|
...CORRECTION...
On sait que la fonction fonctionne en respectant les exemples fournis puisqu'aucune exception n'a été levée.
16° Quelqu'un décide de modifier la fonction. Il décide de remplacer l'utiliser la fonction int() plutôt que round().
- Modifier la ligne 26 de façon à remplacer round() par int().
- Lancer le programme. Voit-on immédiatement que nous avons fait une modification malvenue ?
26 |
|
...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.
17° 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 |
|
...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 |
|
Par contre, si vous devez écrire des instructions conditionnelles, préférez bien évidemment ceci :
1
2
3
4
5 |
|
4 - 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 :
- On place des exemples dans la documentation
- 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.
18° 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.
- Où se trouve le test qui permet d'invalider la fonction pour l'instant ?
- 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.
19° 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 :
20° 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.
5 - 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 !
21° 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().
- 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().
- ligne 230 : On lance la surveillance en boucle de la fenêtre avec la méthode mainloop().
- Fonction floor() du module math
- Fonction ceil() du module math
- Utiliser la fonction native isinstance qui permet de vérifier qu'une variable fait bien référence à un type particulier de contenu :
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)
|
229 | fen_princ.bind('<Button-1>',choix_case)
|
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.
22° 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.
23° 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 !
24° 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 == "?"
|
6 - FAQ
Les arrondis inférieur et supérieur du module math
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
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 :
>>> 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"
|
Activité publiée le 28 08 2019
Dernière modification : 04 09 2024
Auteur : ows. h.