Aller au contenu

Architecture avec des fonctions

Réécriture nécessaire

Ce chapitre et long, dense, et aborde des notions compliquées. Il doit subir une réécriture.

Attention

Ce chapitre présente de nombreuses notions qui sont peu intuitives, et donc qui demandent potentiellement du temps et de la pratique pour être comprises. Ne soyez pas surpris si vous trouvez certains aspects un peu confus n'hesitez pas à demander de l'aide, et experimentez a minima avec les exercices.

Ce chapitre, un peu difficile, est la dernière ligne droite avant de commencer à faire des programmes plus intéressants. Tenez bon !

Rappelez vous, le chapitre précédent, nous avons comparé les fonctions, qui résument une série d'opérations, à l'expression faire des crêpes qui résume une recette.

Toutefois, dire que les fonction résument une série d'opérations est un peu réducteur, tout comme dire que faire des crêpes résume une recette.

En effet, quand on dit faire des crêpes, on ne parle pas des détails d'une recette, mais du concept général de l'action de faire des crêpes. Quand on exprime notre intention de faire des crêpes, ou son résultat, on ne dit pas comment on les a fait.

On s'affranchit donc des détails peu importants (quantité et type de farine, d'oeufs, temps de cuisson, etc) et on ne garde que l'essentiel. C'est ce que l'on appelle une abstraction.

Le processus qui crée les abstractions est l'abstraction.

L'abstraction est un concept central de l'informatique, peut être même LE concept central. Elle passe, entre autres, par l'usage de fonctions.

L'abstraction appartient en majeure partie au domaine de l'architecture logicielle. C'est une discipline qui étudie comment structurer un programme à l'aide, entre autres, d'abstractions, pour le rendre plus facile comprendre et à modifier, plus robuste (moins sujet aux bugs), et voir plus performant.

Dans ce chapitre, nous allons voir une utilisation possible des fonctions pour mieux exprimer nos programmes et donc les rendre plus simples à travailler.

Sous-problèmes, spécifications, rôles

Pour créer des programmes d'un taille importante sans se casser les dents sur leur complexité, on va utiliser l'abstraction permise par les fonctions pour travailler uniquement sur des petits problèmes relativement simples. Chaque fonction sera responsable d'apporter une solution à un problème bien précis dans notre programme.

Si on problème est trop compliqué pour être résolu par une seule fonction, il faut le découper en sous-problèmes. Chaque sous problème sera alors résolu par une fonction, et la fonction qui résoud le "gros problème" fera appel aux fonctions des sous problèmes.

Problèmes et fonctions

Un problème composé de beaucoup de sous problèmes est compliqué, voir complexe, alors qu'une fonction qui résouds un problème compliqué ou complexe en appelant des fonction qui résolvent les sous problèmes est généralement simple.

Pour chaque problème, on va donc spécifier une fonction correspondante, c'est à dire détailler le problème que la fonction doit résoudre, et de quelle manière on veut appeler la fonction (nom et arguments).

Exemple : 

je veux une fonction "encadrement" qui prends en paramètres 
    une chaîne de caractères "s", 
    un entier "n" et 
    une chaine de caractère "c", 
et qui retourne s encadrée de n fois c de chaque côté.

encadrement("cadre", 2, "#!") -> "#!#!cadre#!#!" 

C'est le principe de l'abstraction : on n'a pas besoin de savoir comment une fonction fonctionne pour l'utiliser, mais seulement de connaître sa spécification (parfois abrégé spec ou specs). On va donc pouvoir, pour implementer chaque fonction, se focaliser uniquement sur le problème qu'elle résoud sans avoir à trop se préocuper des autres fonctions et problèmes.

Ainsi, pour implémenter la fonction rendre(monde), je n'ai pas besoin de savoir comment sont (ou vont être) implémentées les fonctions afficher et composer, mais seulement besoin de savoir que j'ai besoin d'elles, et comment les appeler.

Fonctions et abstraction

Il arrive que la spécification d'une fonction soit trop complexe pour être exprimée simplement par le nom de la fonction et de ses arguments. C'est très souvent le cas en Python. On précisera alors la spécification sous forme d'un commentaire spécial appelé docstring, (documentation string en anglais, ou chaîne de documentation en français). C'est une chaîne multiligne placée juste sous la ligne def de la fonction.

def une_fonction():
    """une docstring
    qui peut faire plusieurs lignes
    """
    pass

Dans cette docstring, on va exprimer d'éventuelles contraintes qui doivent être respectées quand on appelle la fonction (par exemple le type des arguments) : c'est qu'on appelle la précondition.

On va aussi exprimer ce que la fonction fait ou retourne si la précondition est respectée, c'est la postcondition.

On ajoutera aussi, eventuellement, quelques exemples d'utilisation de la fonction.

Par exemple, une implémentation de la fonction encadrement pourrait-être :

1
2
3
4
5
6
7
8
def encadrement(s, n, c):
    """s une chaine, n un entier, c une chaine,
    retourne s encadrée de n fois c de chaque côté.

    Exemple: 
    encadrement("cadre", 2, "#") retourne "##cadre##"
    """
    return c*n + s + c*n
  • La ligne 2 est une précondition
  • La ligne 3 une postcondition
  • La ligne 6 un exemple d'utilisation

Dans cet exemple, la fonction est triviale, et on aurait probablement pu se passer de docstring. Mais on verra dans la suite que la docstring peut-être cruciale quand la fonction est plus compliquée, et donc que lire et analyser son code pour la comprendre serait extrêmement coûteux.

Point vocabulaire

Les spécifications portent parfois d'autres noms en fonction du contexte.

On utilise parfois le terme prototype d'une fonction, (et de prototypage comme action de créer un prototype) . Ce terme est à connaître, même si il a un sens qui n'est pas forcément toujours très clair en fonction du contexte. Il renvoie en général à son utilisation dans le contexte du C et du C++, où il définit la manière avec laquelle une fonction peut-être correctement appelée du point de vue du compilateur et non du programmeur, et où il n'inclut que le nom de la fonction et le type et de la position de ses arguments.

On utilise aussi le terme d'interface d'une fonction. Même si il est correct, interface renvoie à énormément d'autres concepts en informatique et dans différents langages de programmation, il peut donc facilement prêter à confusion.

Finalement, on parle plus rarement d'interface de programmation d'une fonction, en anglais API (Application Programming Interface), qui renvoie au protocoles ou conventions utilisées par un programmeur pour appeler une fonction par le biais d'un logiciel. Toutefois l'usage dans ce contexte est relativement rare et peut prêter à de fortes confusions dans notre cas.

Exercices

Exercice : max3

Ecrivez en Python la spécification d'une fonction qui prends en entrée trois entiers positifs, et retourne le plus grand des trois.

Rappel de la spécification :

def nom_de_la_fonction(argument, argument, ...):
    """une chaine qui complete 
    la spécification de la fonction
    """
    pass ## ligne pas obligatoire ici mais mieux pour que le prog. soit valide

n'oubliez pas de fournir : la précondition, la postcondition, et eventuellement un exemple ou deux

il n'est pas demandé d'implanter la fonction, seulement de la spécifier

def maximum(a, b, c):
    """ a, b, c, des entiers positifs
    retourne le plus grand nombre des trois

    Exemple : 
    maximum(10, 12, 3) retourne 12
    maximum(20, 3, 7) retourne 20
    """

On appelle la fonction maximum. D'autres noms auraient aussi pu faire l'affaire, comme max3, maximum3, maxi, plus_grand, ...

Exercice : somme et produit

Proposez une décomposition en sous problèmes du problème suivant :

On veut créer un programme qui demande à l'utilisateur deux nombres, puis affiche, sur deux lignes différentes, leur somme et leur produit.

On utilise souvent les mots "puis" et "et" comme des séparateurs de sous problèmes, sans nous en rendre compte.

Il y a de nombreuses solutions possibles, en voici une :

  • demander deux nombres à l'utilisateur, puis afficher leur somme et leur produit sur des lignes différentes
    • afficher la somme des deux nombres
    • afficher le produit des deux nombres
    • afficher une nouvelle ligne

la décomposition en sous problèmes n'est pas toujours aussi évidente.

Une autre solution peut-être : - demander deux nombres à l'utilisateur, puis afficher leur somme et leur produit sur des lignes différentes - calculer la somme de deux nombres - calculer le produit de deux nombres - Afficher deux chaines de caractères sur des lignes différentes

Exercice : somme et produit, suite

A partir de la décomposition en sous problème de l'exercice précédent, spécifiez les fonctions qui résolvent chacun des sous problèmes.

Rappel de la décomposition :

  • demander deux nombres à l'utilisateur, puis afficher leur somme et leur produit sur des lignes différentes
    • afficher la somme des deux nombres
    • afficher le produit des deux nombres
    • afficher une nouvelle ligne

Ensuite, dites quelle fonction appelera quelles autres fonctions, sans pour autant implanter les fonctions en question.

Il y a une fonction par sous problème.

def afficher_somme_produit():
    """Demande deux nombres à l'utilisateur puis affiche leur somme et leur produit sur deux lignes différentes"""

def afficher_somme(a, b):
    """a, b des nombres,
    affiche la valeur de a + b
    """
    pass

def afficher_produit(a, b):
    """a, b des nombres,
    affiche la valeur de a * b
    """
    pass

def nouvelle_ligne():
    """affiche une ligne vide"""
    pass

Cette solution n'est pas la seule possible. Entre autres, on pourrait par exemple considérer que le problème d'affichage d'une nouvelle ligne est déjà résolu par print, qui fait automatiquement un retour à la ligne à chaque affichage.

Assertions

Python vérifie implicitement une partie précondition d'une fonction quand on l'appelle.

Par exemple, si on appelle une fonction avec trop (ou trop peu) d'arguments, Python lève une erreur :

def addition(a, b):
    return a + b

addition(10)

Résulte en une erreur :

Traceback (most recent call last):
  File "/home/felix/exemple_test_2.py", line 4, in <module>
    addition(10)
TypeError: addition() missing 1 required positional argument: 'b'

addition() missing 1 required positional argument: 'b' veut dire addition demande un argument supplémentaire : b

def addition(a, b):
    return a + b

addition(10, 20, 30)
Résulte également en une erreur :

Traceback (most recent call last):
  File "/home/felix/exemple_test_2.py", line 4, in <module>
    addition(10, 20, 30)
TypeError: addition() takes 2 positional arguments but 3 were given

addition() takes 2 positional arguments but 3 were given veut dire addition demande 2 arguments, mais en a reçu 3.

Ce genre de vérifications permettent d'éviter des erreurs d'inatention.

Les vérifications effectuées implifitement par Python sont toutefois limitées. Par exemple, si on fait une fonction qui effectue la division de deux flottants et retourne le résultat :

def division(a, b):
    """a et b des flottants. b != 0 
    retourne a / b
    """
    return a/b

Python ne voit aucun problème à appeler la fonction avec 0 comme valeur de b, ce qui crée une erreur de division par 0 :

division(10, 0)
Traceback (most recent call last):
  File "/home/felix/exemple_test_2.py", line 7, in <module>
    division(10, 0)
  File "/home/felix/exemple_test_2.py", line 5, in division
    return a/b
ZeroDivisionError: division by zero

Face à cette erreur, il est difficile de savoir si c'est la fonction division qui est mal implantée, où si c'est notre appel qui ne respecte pas la précondition.

On pourraît modifier notre fonction pour qu'elle affiche un message si elle reçoit la valeur 0 pour b, en ajoutant un if:

def division(a, b):
    """a et b des flottants. b != 0.
    retourne a / b
    """
    if b == 0.0:
        print("Précondition non vérifiée : b ne doit jamais être 0. Lisez la docstring SVP.")

    return a/b

Le problème est qu'il est difficile de savoir si le if fait partie des opérations faites par la fonction pour produire un résultat ou une action, où si c'est juste une vérification de précondition.

Python nous fournis donc un outils sémantique, les assertions, qui expriment une vérification d'une condition qui inutile pour le résultat et sert simplement à vérifier que le programme s'execute dans les conditions pour lesquelles il a été prévu.

Une assertion s'écrit assert condition. Si la condition est vérifiée, l'assertion ne fait rien. Sinon, elle lève une erreur de type AssertionError.

Dans le code suivant :

a = 10
print("AVANT !")
assert a < 20
print("APRES !")
La condition de l'assertion est vérifiée, elle ne fait donc rien. On dit que l'assertion réussit.
AVANT !
APRES !

Alors que dans ce code :

a = 10
print("AVANT !")
assert a > 20
print("APRES !")

La condition de l'assertion n'est pas véfifiée, et elle lève une erreur. On dit que l'assertion échoue.

AVANT !
Traceback (most recent call last):
  File "/home/felix/exemple_test_2.py", line 3, in <module>
    assert a > 20
AssertionError

On peut optionnellement ajouter un message aux assertions, sous forme d'une chaîne de caractère séparée de la condition par une virgule :

a = 10
print("AVANT !")
assert a > 20, "A n'est pas inférieur à 20 alors qu'il le devrait !!!"
print("APRES !")

Le message sera affiché dans la trace de l'erreur :

AVANT !
Traceback (most recent call last):
  File "/home/felix/exemple_test_2.py", line 3, in <module>
    assert a > 20, "A n'est pas inférieur à 20 alors qu'il le devrait !!!"
AssertionError: A n'est pas inférieur à 20 alors qu'il le devrait !!!

On peut maintenant vérifier automatiquement tout ou une partie de la précondition d'une fonction.

def division(a, b):
    """a et b des flottants. b != 0.
    retourne a/b
    """
    assert b != 0.0, "b ne doit jamais être 0. Lisez la docstring SVP."
    return a/b

Exercices

Exercice : minimum

Ajoutez des assertions dans la fonction suivante pour vérifier que sa précondition est bien respectée.

def minimum(a, b, c):
    """a, b, c des nombres entiers strictement supérieurs à zéro
    retourne la valeur du plus petit d'entre eux
    """
    if a <= b and a <= c:
        return a
    elif b <= c:
        return b
    else:
        return c

Le corps de la fonction n'a pas d'importance dans notre cas, concentrez vous sur la chaîne de spécification.

"""a, b, c des nombres entiers strictement supérieurs à zéro
retourne la valeur du plus petit d'entre eux
"""

Remarque : appliquez la spécification MEME SI DES POINTS SEMBLENT INUTILES

On ajoute des assertions pour vérifier le type et la valeur des arguments a, b et c. Les messages d'erreurs sont optionnels, ici, on a pas vraiment besoin d'en mettre parce que les assertions sont claires sur ce qu'elles testent.

On ne vérifie qu'une seule valeur par condition pour que les erreurs soient plus précises.

def minimum(a, b, c):
    """a, b, c des nombres entiers strictement supérieurs à zéro
    retourne la valeur du plus petit d'entre eux
    """
    assert type(a) == int
    assert type(b) == int
    assert type(d) == int
    assert a > 0
    assert b > 0
    assert c > 0

    if a <= b and a <= c:
        return a
    elif b <= c:
        return b
    else:
        return c

Remarque : quand on a ajouté des assertions, les aspects de la spécifications qu'elles expriment peuvent généralement être retirés de la docstring :

"""a, b, c des entiers
retourne la valeur du plus petit d'entre eux
"""

Alternativement, on aurait pu grouper les vérifications par argument ou par thème (type ou valeur) pour garder le bloc des assertions plus compact, mais cela rends les erreurs moins lisibles, vu qu'on ne sait pas quelle vérification d'une même assertion a levé l'erreur

## Version groupée par theme
assert type(a) == int and type(b) == int and type(c) == int
assert a > 0 and b > 0 and c > 0
## Version groupée par variable
assert type(a) == int and a > 0
assert type(b) == int and b > 0
assert type(c) == int and c > 0

Exercice : reconstituer la spec

A partir des assertions dans la fonction suivante, reconstituez les parties manquantes de la docstring, marquées par des ... :

def encadrer(ch1, a, ch2):
    """ch1, ch2 ...
    a ...
    """
    assert type(ch1) == str
    assert type(ch2) == str
    assert type(a) == int or type(a) = float
    pass

Il faut simplement lire les assertions et les retranscrire en français dans la docstring.

1
2
3
4
5
6
7
8
def encadrer(ch1, a, ch2):
    """ch1, ch2 des chaines de caractères ##(assertions lignes 5 et 6 )
    a un entier ou un flottant ##(assertion ligne 7)
    """
    assert type(ch1) == str
    assert type(ch2) == str
    assert type(a) == int or type(a) = float
    pass

Tests automatiques

Remarque

On reparlera des tests plus en détails un peu plus tard. Ici, l'idée est de vous faire découvrir les tests automatiques, que je vais utiliser pour vous aider dans votre progression.

On veut pouvoir vérifier qu'une fonction implante bien la spécification qu'on recherche.

Jusqu'à maintenant, on a vérifié nos programmes en les executant à la main. C'est une méthode acceptable pour des programmes très très simples, mais absolument déraisonnable pour des programmes plus conséquents, parce que tester à la main prends beaucoup trop de temps, et expose au risque d'erreur humaine.

On veut donc écrire des programmes qui font les tests (ou au moins une partie) à notre place. C'est une tâche qui peut se révéler extrêmement difficile dans certains cas, mais qui est souvent très facile dans le cas des fonctions pures : il suffit de donner des arguments à la fonction, et de vérifier que la valeur qu'elle produit est bien celle qu'on attends. Si teste sur les bonnes paires (arguments - résultat), on peut être relativement confiants sur le fait que notre fonction est conforme à sa spécification.

Exemple, si on veut tester la fonction encadrement, un programme de test (peu rigoureux) pourrait ressembler à :

if cadre("", 0, "") != "":
    print("ECHEC")
if cadre("ed", 2, "#") != "##ed##":
    print("ECHEC")

On verra plus tard de comment écrire des tests efficaces et utiles, en particulier, comment trouver quelques paires arguments;résultat pour minimiser le risque d'erreurs. Pour le moment, je vais vous fournir des tests pour les fonctions que je vous demanderai d'implanter. Chaque fichier Python sera structuré en deux parties :

  • Une partie "exercice" où vous devrez créer ou compléter des fonctions
  • Une partie "test", qui contiendra des tests pour vous aider à voir vos erreurs.

Pour certains exercice, il vous faudra donc copier-coller le sujet de l'exercice et le compléter. Ce sera explicitement précisé dans les énoncés.

Voilà un exemple de code d'exercice, seules les lignes 1 à 11 vous concernent, vous pouvez ignorer les autres :

lancer_tests = True ##Mettez a False pour ne pas lancer les tests
afficher_traces = False ##Mettez a True pour afficher la trace complète des erreurs dans le testss

def concatenation(a, b):
    """a une chaine, b une chaine. retourne a + b."""
    pass ## le PASS permet de lancer les tests sans que la fonction soit implantée.

## Les lignes suivantes vous indique que le code des tests commence
## Vous n'avez pas à vous en préoccuper, concentrez vous sur
## La fonction que vous implantez

############################################
#############   PARTIE TESTS   ############# 
############################################

import unittest
if not afficher_traces:
    __unittest = True

def checktype(expr, res, exp):
    assert type(res) == exp, expr + " doit retourner une valeur de type " + exp.__name__ + ", mais a retourné une valeur de type " + type(res).__name__

def checkval_equal(expr, res, exp):
    str_exp = "'" +exp+ "'" if type(exp) == str else str(exp) 
    str_res = "'" +res+ "'" if type(res) == str else str(res) 
    assert res == exp, expr + " doit retourner " + str_exp + ", mais a retourné " + str_res

class Test_concatenation(unittest.TestCase):

    def test_chaines_vides(self):
        res = concatenation("", "")        
        checktype("concatenation('','')", res, str)
        checkval_equal("concatenation('','')", res, "")

    def test_chaines_non_vides(self):
        res = concatenation("aaa", "bbb")
        checktype("concatenation('aaa','bbb')", res, str)
        checkval_equal("concatenation('aaa', 'bbb')", res, "aaabbb")

if __name__ == '__main__':
    s = unittest.TestLoader().loadTestsFromTestCase(Test_concatenation)
    unittest.TextTestRunner().run(s)

Si on lance ce programme, il affiche affiche la sortie suivante, qui est le rapport de tests :

FF
======================================================================
FAIL: test_chaines_non_vides (__main__.Test_concatenation)
----------------------------------------------------------------------
AssertionError: concatenation('aaa','bbb') doit retourner une valeur de type str, mais a retourné une valeur de type NoneType

======================================================================
FAIL: test_chaines_vides (__main__.Test_concatenation)
----------------------------------------------------------------------
AssertionError: concatenation('','') doit retourner une valeur de type str, mais a retourné une valeur de type NoneType

----------------------------------------------------------------------
Ran 2 tests in 0.000s

FAILED (failures=2)

Essayons de le comprendre.

La ligne 1 donne un résumé très rapide: FF veut dire que deux tests ont étés exécutés, et qu'ils ont tous les deux échoués (Fail en anglais).

Il y a ensuite deux blocs, qui donne chacun le résultat d'un test.

2
3
4
5
======================================================================
FAIL: test_chaines_non_vides (__main__.Test_concatenation)
----------------------------------------------------------------------
AssertionError: concatenation('aaa','bbb') doit retourner une valeur de type str, mais a retourné une valeur de type NoneType
======================================================================
FAIL: test_chaines_vides (__main__.Test_concatenation)
----------------------------------------------------------------------
AssertionError: concatenation('','') doit retourner une valeur de type str, mais a retourné une valeur de type NoneType

Ces deux blocs ont la même structure. Prenons donc le bloc qui commence ligne 7. La ligne

FAIL: test_chaines_vides (__main__.Test_concatenation)

Indique quel test à été exécuté. Le nom est structuré comme suit :

Statut ignorer nom du cas testé ignorer nom de la fonction testée
FAIL : test_ test_chaines_vides (__main__.Test_ concatenation )

Le statut du test est FAIL (échec) ou PASS (succès). Le nom de la fonction testée est exactement le même que celui de la fonction testée. Le nom du cas testé est un résumé de ce que fait le test. Ici par exemple, on teste la fonction concatenation en lui passant en argument des chaînes de caractères vides ("").

Ensuite, la ligne 5 et les suivantes nous donnent des informations sur ce qui a échoué dans le test :

AssertionError: concatenation('','') doit retourner une valeur de type str, mais a retourné une valeur de type NoneType
Le message est normalement assez explicite ici : on donne l'expression qui a été utilisée pour l'appel, le comportement attendu, et le comportement qui a réellement eu lieu. Dans notre cas, à cause de l'instruction pass, concatenation('','') a retourné None au lieu de retourner une valeur de type str.

Essayon de corriger celà, en modifiant la fonction concatenation:

4
5
6
def concatenation(a, b):
    """a une chaine, b une chaine. retourne a + b."""
    return "une chaine."

On relance le programme :

FF
======================================================================
FAIL: test_chaines_non_vides (__main__.Test_concatenation)
----------------------------------------------------------------------
AssertionError: concatenation('aaa', 'bbb') doit retourner 'aaabbb', mais a retourné 'une chaine.'

======================================================================
FAIL: test_chaines_vides (__main__.Test_concatenation)
----------------------------------------------------------------------
AssertionError: concatenation('','') doit retourner '', mais a retourné 'une chaine.'

----------------------------------------------------------------------
Ran 2 tests in 0.000s

FAILED (failures=2)

L'erreur a changé :

======================================================================
FAIL: test_chaines_vides (__main__.Test_concatenation)
----------------------------------------------------------------------
AssertionError: concatenation('','') doit retourner '', mais a retourné 'une chaine.'

Ici, on voit que le test nous demande de retourner une chaîne vide '', mais que notre programme a retourné la valeur 'une chaîne.', qui est bien du type string, mais n'a pas la valeur exacte attendue.

Re-modifions encore la fonction.

4
5
6
def concatenation(a, b):
    """a une chaine, b une chaine. retourne a + b."""
    return ""

Le programme affiche alors :

F.
======================================================================
FAIL: test_chaines_non_vides (__main__.Test_concatenation)
----------------------------------------------------------------------
AssertionError: concatenation('aaa', 'bbb') doit retourner 'aaabbb', mais a retourné ''

----------------------------------------------------------------------
Ran 2 tests in 0.000s

FAILED (failures=1)

On voit que la ligne 1 a changé : F. indique que deux tests ont étés exécutés, et que un a échoué (F), et un a réussi (.).

Le test chaines_vides n'est plus mentionné, parce qu'il est passé avec succés !

Si on modifie la fonction encore une fois, pour qu'elle soit conforme à sa spécification,

4
5
6
def concatenation(a, b):
    """a une chaine, b une chaine. retourne a + b."""
    return a + b

On voit que tous les tests sont passés :

1
2
3
4
5
..
----------------------------------------------------------------------
Ran 2 tests in 0.000s

OK

Conseil

Les tests sont là pour vous aidez à trouver vos erreurs et vous corriger. Essayez de ne pas faire du die and retry, en testant très souvent et en faisant de petites modifications sans réfléchir jusqu'à ce que ça marche : essayez plutôt d'implanter complètement une fonction à partir de sa specification avant de lancer les tests.

En haut de chaque fichier contenant des tests se trouveront des booléens pour contrôler l'execution des tests. En particulier :

  • lancer_tests, contrôle si les tests sont exécutés (True) ou ignorés (False).
  • afficher_traces, si mis à True, affiche les traces d'appel des erreurs dans les tests. Ca peut être intéressant au cas où vous voulez débugger des erreurs qui sont internes à la fonction que vous écrivez (par exemple une division par zéro), mais celà rendra la sortie des tests bien plus difficile à lire.

Exercices

Exercice : rapport de tests

Observez le rapport de tests ci-dessous et répondez aux questions :

.F.
======================================================================
FAIL: test_soir (__main__.Test_bonjour)
----------------------------------------------------------------------
AssertionError: bonjour("soir") doit retourner 'bonsoir', mais a retourné 'bonne nuit'

----------------------------------------------------------------------
Ran 3 tests in 0.000s

FAILED (failures=1)

Le but de cet exercice est de comprendre le rapport de test sans se préocuper du code qu'il teste.

Combien de tests ont étés exécutés ?

La ligne 1 comporte 3 caractères

.F.
On peut en déduire que 3 tests ont étés exécutés.

Alternativement, la ligne 8 nous indique 3 tests ont étés exécutés :

Ran 3 tests in 0.000s

Combien de tests ont échoué ?

La ligne 1 comporte 1 caractère F

.F.
On peut en déduire que 1 a échoué.

Alternativement, la ligne 10 nous indique 1 test a échoué :

FAILED (failures=1)

Quelle fonction n'a pas passé les tests (= un de ses tests a échoué) ?

La ligne 3 comporte des informations sur le test. linenums=3 FAIL: test_soir (__main__.Test_bonjour)

En particulier, la partie 
__main__.Test_bonjour
indique quelle fonction a échoué : c'est la fonction `bonjour`, avec la syntaxe suivante : 
__main__.Test_nom_de_la_fonction

Cette convention est propre à ce cours, nous la conserveront jusqu'à ce que nous ayons vu les tests plus en détail.

Quel est le nom du cas qui a échoué ?

La ligne 3 comporte des informations sur le test. linenums=3 FAIL: test_soir (__main__.Test_bonjour)

En particulier, la partie 
test_soir nous indique le cas qui a échoué : ici, soir.

Le cas d'un test ne sera pas toujours explicite, mais il peut résumer rapidement ce qui a été testé dans un test et donc permettre une compréhension plus rapide du problème.

Quel appel a effectivement été effectué par le test ? Quelle valeur l'appel a-t-il renvoyé ? Quelle valeur était attendue ?

La ligne 5 nous renseigne sur ce que le test a essayé de faire et qui a échoué :

AssertionError: bonjour("soir") doit retourner 'bonsoir', mais a retourné 'bonne nuit'

L'appel

bonjour("soir")

a été testé. On voulait qu'il renvoie la valeur "bonsoir", mais il a retourné bonne nuit.

Portée des variables

On a vu que les fonctions sont des boîtes noires, des machines opaques, qu'on peut utiliser sans en connaître le fonctionnement exact.

Mais cette opacité va plus loin : ce qui est dans le corps d'une fonction est isolé du reste du programme.

Essayons un petit bout de code :

1
2
3
4
def une_fonction():
    une_variable = 0

print(une_variable)

Ce code génère une erreur :

Traceback (most recent call last):
  File "/home/felix/exemple_test_2.py", line 4, in <module>
    print(une_variable)
NameError: name 'une_variable' is not defined

une_variable n'est pas définie à la ligne 4.

Quand on définit une variable dans une fonction, elle n'est définie que dans la fonction. C'est ce que l'on appelle la portée d'une variable. Celà veut dire que si deux variables on le même nom dans deux fonctions différentes, ou qu'une variable dans une fonction a le même nom qu'une variable hors fonction, elles ne représentent PAS la même boîte.

def une_fonction(b):
    une_variable = b
    print(b)

une_variable = 1
une_fonction(10)
print(une_variable)

Produit :

10
1

On dit que les variables qui sont dans le corps d'une fonctions sont locales à la fonction, et que les variables qui sont hors d'une fonction sont globales.

La localité a de nombreux avantages. L'un d'entre eux est qu'elle facilite la compréhension du code. Une fonction et sa spécification posent un contexte qui donne donc des indices sur ce que fait le code : on l'aborde avec un a priori qui nous donne des pistes d'analyse. Celà permet par exemple de proposer des noms de variables plus courts sans pour autant perdre en clarté.

## ce code utilise une fonction pour donner un contexte

## Parce que la fonction s'appelle dessiner_rectangle, 
## on a une idée de ce que doit produire le code,
## et on se doute que "largeur" et "hauteur" sont les dimensions du rectangle

def dessiner_rectangle(largeur, hauteur):
    print(("#" * largeur + "\n") * hauteur)

dessiner_rectangle(8, 10)
## Ce code n'utilise pas une fonction comme contexte

## En l'absence de contexte de lecture, 
## on doit utiliser des commentaires ou des noms de variables plus longs
## pour que le code soit aussi compréhensible

# On dessine un rectangle
largeur_rectangle = 8
hauteur_rectangle = 10

print(("#" * largeur_rectangle + "\n") * hauteur_rectangle)

## Le code est alors plus long, et donc plus coûteux à lire.
## Ce code n'utilise pas une fonction comme contexte

## Si on ne veut pas allonger le code par des commentaires
## ou des précisions sur le nom des variables, le code
## devient dur à analyser : on ne sait pas directement ce qu'il
## est sensé produire 

largeur = 8
hauteur = 10

print(("#" * largeur + "\n") * hauteur)

Il faut donc éviter d'avoir des variables ou des instructions globales dans notre code, sauf exceptions très rares.

Python est un langage qui met en priorité absolu la facilité d'utilisation. Il execute donc un fichier en lisant les instructions globales dans l'ordre où elles apparaissent. La plupart des langages interdisent d'avoir des instructions en dehors de fonction, et proposent à la place une fonction principale, généralement appelée main (mot anglais pour principal), qui sert de point d'entrée dans le programme : quand on lance le programme, la fonction main est appelée. Voici un exemple en C++ :

#include <iostream>

int main(int argc, char** argv){
    std::cout << "Hello, World!" << std::endl;
}

C'est une pratique qui est très structurante, et nous assure, entre autres, de ne pas avoir des instructions éparpillées partout dans notre code. Python propose donc une sémantique similaire :

if __name__ == "__main__":
    ##ici les instructions de la "fonction" main

Par exemple, il est préférable d'écrire le programme suivant :

def dessiner_rectangle(largeur, hauteur):
    print(("#" * largeur + "\n") * hauteur)

dessiner_rectangle(8, 10)

Comme ceci :

def dessiner_rectangle(largeur, hauteur):
    print(("#" * largeur + "\n") * hauteur)*


if __name__ == "__main__":
    dessiner_rectangle(8, 10)

Optionellement, vous pouvez créer une fonction main et l'appeler dans la condition, plutôt que d'écrire directement les instructions :

def dessiner_rectangle(largeur, hauteur):
    print(("#" * largeur + "\n") * hauteur)*

def main():
    dessiner_rectangle(8, 10)

if __name__ == "__main__":
    main()

Modules, bibliothèques

Certains problèmes nécessitent parfois plusieurs fonctions pour être résolus.

Par exemple, le problème "afficher un monde en 2D" se compose d'au moins deux sous problèmes, chacun correspondant à une fonction :

  • Composer le monde (fonction composer)
  • Afficher une forme (fonction afficher)

Spécifier la solution à un tel problème nécessite donc de spécifier non pas une fonction, mais un groupe de fonctions. Les fonctions du groupe ne peuvent pas être spécifiées complètement indépendamment, parce qu'on veut que les formes qui composent le monde créé par la fonction composer puissent-être affichées avec la fonction afficher.

fonctions_dependantes

Un groupe cohérent de fonction s'appelle un module. Un module peut même contenir d'autres modules, et des composants logiciels qui ne sont pas des fonctions.

module

La spécification d'un module est en général la somme de la spécifiation de ses composants. Comme pour une fonction, un module est une sorte de boîte noire : on n'a pas besoin de comprendre autre chose que sa spécification pour l'utiliser.

specification module

Organiser un programme sous forme de modules s'appelle la programmation modulaire, et est au programme de terminale et pas au programme de première. Toutefois, il est intéressant de commencer à réfléchir de temps en temps à cette approche, puisqu'il est probable que vous ayez des réflexions proches durant vos projets.

La programmation modulaire a de nombreux avantages. L'un d'eux est le fait que les modules facilitent la réutilisation, c'est à dire le partage de code entre programmeurs pour éviter de résoudre deux fois le même problème.

Les modules sont souvent partagés sous forme de bibliothèques logicielles (software libraries en anglais, souvent abrégé libs).

Remarque

Les modules sont parfois appelés bibliothèques, ou paquets (packages en anglais) et vice versa. Même si leur sens est différent, dans de nombreux contexte, et dans le contexte de la programmation en Python, cette différence est négligeable.

Une bibliothèque logicielle est simplement une collection de fonctions qui partagent un thème commun (par exemple, le traitement d'image), et qui sont généralement organisées en modules.

Voici quelques exemples de bibliothèques logicielles (ou de modules) que l'on trouve en Python:

Nom Utilisation
Pygames Jeu vidéo
Pandas Analyse de données
PyQt Interfaces graphiques
Django Développement web
Numpy Analyse de données, calcul
Scipy Calcul scientifique, utilise Numpy
turtle Dessin simplifié de formes

Tous les langages de programmations ou presques spécifient une bibliothèque standard. C'est une bibliothèque qui est fournie avec le langage, et propose des fonctions courantes qui n'ont pas étés exprimées directement dans la syntaxe du langage. Voici quelques modules de la bibliothèque standard de Python.

Nom Utilisation
turtle Dessin simplifié de formes
random Générations de nombres pseudo-aléatoires
unittest Tests automatiques. Je l'utilise pour les tests présentés plus haut.
math Calcul

Pour utiliser un module, il faut demander à Python de l'importer, en utilisant le mot clé import.

import nomdumodule

Par exemple, pour importer le module random:

import random

Pour accéder aux fonctions et autres composants au sein d'un module, il faut utiliser l'opérateur ..

import random

## On utilise la fonction randint du module random
## tire un nombre aléatoire entre 0 et 9 inclus
res = random.randint(0, 9)
print(res)
Une sortie possible du programme :
9

Pour éviter d'utiliser la notation avec un ., on peut spécifier quels fonction importer avec la syntaxe suivante : from module import composant .

## on peut spécifier plusieurs composants en les séparants avec une virgule
from random import randint, random

## la fonction random du module random renvoie un float entre 0 et 1.0
print(random())

print(randint(1, 20))
Une sortie possible :
0.5283468863272605
19

Le from import n'est pas toujours une bonne idée, parce qu'il nous empèche d'utiliser certains noms. Parfois, on préférera donner un alias au module pour raccourcir son non, avec le mot clé as.

import random as rd

res = rd.randint(0, 9)
print(res)
une sortie possible
8

Pour connaître quelles fonctions sont dans un module, il faut aller voir sa documentation. Chercher python nomdumodule dans un moteur de recherche permet en général de la trouver. Par exemple, la page de documentation du module random se trouve à l'adresse https://docs.python.org/fr/3/library/random.html.

Les bibliothèques jouent un rôle crucial en informatique : c'est parce qu'on n'a pas à réimplémenter encore et encore les mêmes fonctions pour des programmes différents qu'on arrive à faire des programmes de plus en plus complexes.

Python, qui est un langage avec une grande communauté, donne accès à une quantité de bibliothèques énorme. La bibliothèque standard de Python est elle-même très fournie, nous allons en voir une partie dans les chapitres suivants.

TP : turtle

La plupart des exercices de ce TP utilisent le module turtle. Le principe de ce module est de déplacer une tortue dans une fenêtre. A chaque fois que la tortue se déplace, elle va tracer une ligne le long de son chemin. On peut donc la piloter et tracer des formes relativement complexes.

La tortue se déplace dans un repère en 2D, qui utilise donc deux coordonnées, x (axe horizontal) et y (axe vertical). Elle est toujours positionnée en (0,0) au démarrage du programme.

Il existe deux manières de diriger la tortue :

  • En lui indiquant, un déplacement relatif, sous forme d'une orientation et une direction à suivre à partir de sa position: tourne de 70 degrés sur la gauche et avance de 100.
  • En lui indiquand une position absolue que la tortue doit atteindre. va en (100,100). Elle s'y rends alors en ligne droite.

On peut bien entendu combiner les deux modes de pilotage. Déplacer la tortue en un point précis est beaucoup plus facile avec une position absolue. Créer des polygones réguliers comme des pentagones est en général beaucoup plus simple avec une orientation et une direction.

Voici un exemple de pilotage de tortue :

from turtle import forward, left

forward(200) ## Avancer de 200
left(90) ## Angle droit à gauche
forward(200)
left(90)
forward(200)
left(90)
forward(200)
left(90) #on remet la tortue en orientation initiale !

from turtle import setpos

setpos(200, 0) ## la tortue va en (200,0) setpos(200, 200) setpos(0, 200) setpos(0, 0)

Dans les deux cas, il est difficile de voir ce que fait le code en un coup d'oeil. On va donc utiliser des fonctions pour simplifier et expliciter le code.

Partie 1 : Rectangle relatif

Implémentez la fonction suivante :

def rectangle(l, h):
    """l, h des entiers.  
    Utilise le pilotage relatif (orientation et direction) pour tracer un rectangle de largueur l et de hauteur h.
    A la fin du tracé, la tortue est de retour a sa position et son orientation d'origine.
    """
    pass

Le code de la fonction sera très similaire à l'exemple de pilotage relatif proposé plus haut.

N'oubliez pas de réorienter une dernière fois la tortue.

N'oubliez pas, si ce n'est pas déjà fait, d'importer le module et les fonctions dans votre code.

Appelez votre fonction pour la tester.

La tortue est orientée vers la droite de l'écran, elle va donc se déplacer dans la largeur de l'écran, puis tourner de 90 degrés à sa gauche pour se déplacer vers le haut, et donc la hauteur de l'écran.

from turtle import forward, left

def rectangle(l, h):
    """l, h des entiers.  
    Utilise le pilotage relatif (orientation et direction) pour tracer un rectangle de largueur l et de hauteur h.
    A la fin du tracé, la tortue est de retour a sa position et son orientation d'origine.
    """
    forward(l) ## Avancer de l
    left(90) 
    forward(h) ## avancer de h
    left(90)
    forward(l)
    left(90)
    forward(h)
    left(90) #on remet la tortue en orientation initiale !

if __name__ == "__main__":
    rectangle(300, 120)
    rectangle(130, 200)

On aimerait bien dessiner des rectangles un peu partout sur la surface, et pas seulement avec l'angle inférieur gauche sur (0,0).

Pour ça, on peut déplacer la tortue en utilisant setpos. Importez setpos de turtle et modifiez votre main pour ajouter un appel à setpos entre les deux rectangles :

from turtle import forward, left, setpos

## ... Ici la fonction Rectangle

if __name__ == "__main__":
    rectangle(300, 120)
    setpos(-100, -300)
    rectangle(130, 200)

C'est pas mal, mais il y a un soucis. Quand on fait setpos, la tortue continue de tracer. On voudrait :

  • Dire à la tortue d'arrêter de tracer. On utilisera la fonction penup ("lever le stylo"), qui ne prends aucun argument.
  • Deplacer la tortue
  • Dire à la tortue de reprendre le tracé. On utilisera la fonction pendown ("baisser le stylo"), qui ne prends aucun argument.

Partie 2 : Déplacer la tortue

Implémentez la fonction suivante, et ajoutez là à votre code :

def deplacer(x, y):
    """x, y des entiers
    deplace la tortue aux coordonnées (x,y) sans tracer de trait, et sans
    changer son oritentation
    """
    pass

N'oubliez pas d'importer les fonctions nécessaires !

Dans le main, appelez la fonction deplacer au lieu de la fonction setpos.

Importez les fonctions penup et pendown.

Vous n'avez simplement que 3 appels de fonction à faire, ils sont plus ou moins décrits dans les explications juste avant l'exercice.

from turtle import forward, left, setpos, penup, pendown

##... ICI la fonction Rectangle

def deplacer(x, y):
    """x, y des entiers
    deplace la tortue aux coordonnées (x,y) sans tracer de trait, et sans
    changer son oritentation
    """
    penup()
    setpos(x,y)
    pendown()

if __name__ == "__main__":
    rectangle(300, 120)
    deplacer(-100, -300)
    rectangle(130, 200)
Code complet
    from turtle import forward, left, setpos, penup, pendown

    def rectangle(l, h):
        """l, h des entiers.  
        Utilise le pilotage relatif (orientation et direction) pour tracer un rectangle de largueur l et de hauteur h.
        A la fin du tracé, la tortue est de retour a sa position et son orientation d'origine.
        """
        forward(l) ## Avancer de l
        left(90) 
        forward(h) ## avancer de h
        left(90)
        forward(l)
        left(90)
        forward(h)
        left(90) #on remet la tortue en orientation initiale !

    def deplacer(x, y):
        """x, y des entiers
        deplace la tortue aux coordonnées (x,y) sans tracer de trait, et sans
        changer son oritentation
        """
        penup()
        setpos(x,y)
        pendown()

    if __name__ == "__main__":
        rectangle(300, 120)
        deplacer(-100, -300)
        rectangle(130, 200)

On veut pouvoir dessiner des rectangles alignés sur les axes (leurs bords sont parallèles aux axes) de la manière la plus pratique possible pour un humain (c'est à dire qu'on veut faire peu de calculs).

Pour ça, on va tracer des rectangles à partir d'un début et d'une fin. Le début sera le point avec les coordonnées x et y les plus petites, et la fin sera le point avec les coordonnées x et y les plus grandes.

On a pas besoin de plus de points que ça, on peut déduire les deux autres coordonnées.

Partie 3 : un peu de dessin

Soit un rectangle \(ABCD\), dans un repère orthonormé, tel que \((AB)\) est parallèle à l'axe des abscisses \((x)\).

On pose \(A = (0,0)\) et \(C = (100,50)\). Que valent \(B\) et \(D\) ?

N'hésitez pas à tracer \(ABCD\) sur une feuille (à carreaux ça peut aider) ! On fait est en train de faire du dessin, pas de la géométrie !

\(B = (100, 0)\) et \(D = (0, 50)\)

Explications.

On sait que \((AB)\) est parallèle à \((x)\), donc \(B\) a la même ordonnée que \(A\), soit \(0\). Aussi, parce que le rectangle n'a que des angles droits et que le repère est orthonormé, \((BC)\) est parallèle à l'axe des abscisses \((y)\), et a donc \(B\) à la même abscisse que \(C\), soit \(100\).

Même raisonnement pour \(D\).

Une petite vue graphique peut aider à comprendre:

dessin d'un rectangle

On pose \(A = (50,50)\) et \(C = (200,75)\). Que valent \(AB\) et \(CD\), respectivement les longueurs des segments \([AB]\) et \([CD]\) ? Et \(BC\) et \(AD\) ?

Rappel, ce n'est pas de la géométrie mais du dessin ! N'hésitez pas à dessiner et à mesurer !

Pour ceux qui veulent absolument calculer, la formule de distance entre deux points \((x_1, y_1)\) et \((x_2, y_2)\) est

\[ \sqrt{(x_2 - x_1)^2 + (y_2 - y_1)^2} \]

Et on en a pas besoin ici, une simple soustraction suffit !

\(AB = CD = 150\) et \(BC = AD = 25\) On sait que \(A\) et \(B\) ont la même ordonnée, donc seule leur abscisse compte. Alors on a \(200 - 50 = 150\). Comme \(ABCD\) est un rectangle, \(AB = CD\).

Même raisonnement pour \(BC\) et \(AD\).

On fait beaucoup trop de calcul pour une activité de dessin ! Pourquoi ne pas implémenter une fonction pour ne pas avoir à refaire ces calculs à chaque fois ?

Partie 4 : Rectangle Aligné aux Axes

Implémentez la fonction suivante. Ajoutez là bien sûr au reste du code. N'oubliez pas de renseigner les assertions qui correspondent à la spécification !

def rectangle_aa(xd, yd, xf, yf):
    """xd, yd, xf, yf des entiers. xd <= xf, yd <= yf
    Dessine un rectangle ayant pour début (xd, yd) et pour fin (xf, yf)
    """
    pass

Servez-vous des fonctions rectangle et deplacer que nous avons fait précédemment, ainsi que de ce que nous avons vu à l'exercice précédent !

Deplacez la tortue sur le point d'origine du rectangle, puis calculez la longueur et largeur du rectangle. Finalement, tracez le rectangle.

def rectangle_aa(xd, yd, xf, yf):
    """xd, yd, xf, yf des entiers. xd <= xf, yd <= yf
    Dessine un rectangle ayant pour début (xd, yd) et pour fin (xf, yf)
    """
    deplacer(xd, yd)
    rectangle(xf - xd, yf - yd)
Code complet
    from turtle import forward, left, setpos, penup, pendown

    def rectangle(l, h):
        """l, h des entiers.  
        Utilise le pilotage relatif (orientation et direction) pour tracer un rectangle de largueur l et de hauteur h.
        A la fin du tracé, la tortue est de retour a sa position et son orientation d'origine.
        """
        forward(l) ## Avancer de l
        left(90) 
        forward(h) ## avancer de h
        left(90)
        forward(l)
        left(90)
        forward(h)
        left(90) #on remet la tortue en orientation initiale !

    def deplacer(x, y):
        """x, y des entiers
        deplace la tortue aux coordonnées (x,y) sans tracer de trait, et sans
        changer son oritentation
        """
        penup()
        setpos(x,y)
        pendown()

    def rectangle_aa(xd, yd, xf, yf):
        """xd, yd, xf, yf des entiers. xd <= xf, yd <= yf
        Dessine un rectangle ayant pour début (xd, yd) et pour fin (xf, yf)
        """
        deplacer(xd, yd)
        rectangle(xf - xd, yf - yd)

    if __name__ == "__main__":
        rectangle_aa(50, 50, 200, 200)

Finalement, pour dessiner un rectangle coloré, on va s'y prendre comme ça :

  • On appelle la fonction color, qui prends un paramètre, et qui définit à la fois la couleur de trait et la couleur de remplissage. La couleur est une chaîne de caractère qui représente la couleur en anglais, par exemple "red" pour rouge. Alternativement, on peut l'appeler avec trois paramètres entiers de 0 à 255 qui définissent les composantes RVB de la couleur.
  • On appelle la fonction begin_fill qui ne prends aucun paramètre, pour dire qu'on va tracer une forme qu'il faudra remplir.
  • On trace le rectangle
  • On appelle la fonction end_fill qui ne prends aucun paramètre, pour dire qu'on a fini de tracer la forme à remplir.
from turtle import forward, left, setpos, penup, pendown, color, begin_fill, end_fill

## ...  reste de notre code ici 

if __name__ == "__main__":
    color("red")
    begin_fill()
    rectangle_aa(50, 50, 200, 200)
    end_fill()

Partie 5 : Rectangle coloré

Implementer la fonction suivante et ajoutez-la à votre code. N'oubliez pas d'importer les fonctions nécessaires.

def remplir_rect_aa(xd, yd, xf, yf, couleur):
    """xd, yd, xf, yf des entiers. xd <= xf, yd <= yf, couleur une chaine
    Remplit un rectangle ayant pour début (xd, yd) et pour fin (xf, yf)
    avec la couleur couleur
    """

C'est exactement le même principe que l'exemple juste au dessus.

def remplir_rect_aa(xd, yd, xf, yf, couleur):
    """xd, yd, xf, yf des entiers. xd <= xf, yd <= yf, couleur une chaine
    Remplit un rectangle ayant pour début (xd, yd) et pour fin (xf, yf)
    avec la couleur couleur
    """
    color(couleur)
    begin_fill()
    rectangle_aa(xd, yd, xf, yf)
    end_fill()
Code complet
    from turtle import forward, left, setpos, penup, pendown, color, begin_fill, end_fill

    def rectangle(l, h):
        """l, h des entiers.  
        Utilise le pilotage relatif (orientation et direction) pour tracer un rectangle de largueur l et de hauteur h.
        A la fin du tracé, la tortue est de retour a sa position et son orientation d'origine.
        """
        forward(l) ## Avancer de l
        left(90) 
        forward(h) ## avancer de h
        left(90)
        forward(l)
        left(90)
        forward(h)
        left(90) #on remet la tortue en orientation initiale !

    def deplacer(x, y):
        """x, y des entiers
        deplace la tortue aux coordonnées (x,y) sans tracer de trait, et sans
        changer son oritentation
        """
        penup()
        setpos(x,y)
        pendown()

    def rectangle_aa(xd, yd, xf, yf):
        """xd, yd, xf, yf des entiers. xd <= xf, yd <= yf
        Dessine un rectangle ayant pour début (xd, yd) et pour fin (xf, yf)
        """
        deplacer(xd, yd)
        rectangle(xf - xd, yf - yd)

    def remplir_rect_aa(xd, yd, xf, yf, couleur):
        """xd, yd, xf, yf des entiers. xd <= xf, yd <= yf, couleur une chaine
        Remplit un rectangle ayant pour début (xd, yd) et pour fin (xf, yf)
        avec la couleur couleur
        """
        color(couleur)
        begin_fill()
        rectangle_aa(xd, yd, xf, yf)
        end_fill()

    if __name__ == "__main__":
        remplir_rect_aa(50, 100, 200, 300, "red")

Partie 6 : Fonction mystère

Observez la fonction suivante, et essayez de prédire ce qu'elle dessine sans l'executer:

def dessiner(x, y, couleur):
    remplir_rect_aa(x +   0, y +   0, x + 250, y + 400, "black")
    remplir_rect_aa(x + 240, y +  25, x + 330, y + 300, "black")
    remplir_rect_aa(x +  10, y +  10, x + 240, y + 390, couleur)
    remplir_rect_aa(x +   0, y +   0, x + 100, y - 100, "black")
    remplir_rect_aa(x + 150, y +   0, x + 250, y - 100, "black")
    remplir_rect_aa(x +  10, y +  10, x + 90,  y -  90, couleur)
    remplir_rect_aa(x + 160, y +  10, x + 240, y -  90, couleur)
    remplir_rect_aa(x + 250, y +  35, x + 320, y + 290, couleur)
    remplir_rect_aa(x -  15, y + 225, x + 175, y + 325, "black")
    remplir_rect_aa(x -   5, y + 235, x + 165, y + 315, "lightgrey")

Remarque : n'y passez pas trop de temps et n'oubliez pas que l'aide est là !

Peut-être qu'avec un nom plus explicite ce sera plus facile. Après tout, c'est un des intérêts des fonctions...

def dessiner_crewmate(x, y, couleur):
    remplir_rect_aa(x +   0, y +   0, x + 250, y + 400, "black")
    remplir_rect_aa(x + 240, y +  25, x + 330, y + 300, "black")
    remplir_rect_aa(x +  10, y +  10, x + 240, y + 390, couleur)
    remplir_rect_aa(x +   0, y +   0, x + 100, y - 100, "black")
    remplir_rect_aa(x + 150, y +   0, x + 250, y - 100, "black")
    remplir_rect_aa(x +  10, y +  10, x + 90,  y -  90, couleur)
    remplir_rect_aa(x + 160, y +  10, x + 240, y -  90, couleur)
    remplir_rect_aa(x + 250, y +  35, x + 320, y + 290, couleur)
    remplir_rect_aa(x -  15, y + 225, x + 175, y + 325, "black")
    remplir_rect_aa(x -   5, y + 235, x + 165, y + 315, "lightgrey")

Si ce n'est pas déjà fait, essayez d'appeler la fonction avec une position et un paramètre de couleur, par exemple de la manière suivante :

dessiner_crewmate(0,0,"green")

Cette fonction trace un crewmate, personnage d'un jeu bien connu de traîtrise spatiale1.

Code complet si besoin

from turtle import forward, left, setpos, penup, pendown, color, begin_fill, end_fill

def rectangle(l, h):
    """l, h des entiers.  
    Utilise le pilotage relatif (orientation et direction) pour tracer un rectangle de largueur l et de hauteur h.
    A la fin du tracé, la tortue est de retour a sa position et son orientation d'origine.
    """
    forward(l) ## Avancer de l
    left(90) 
    forward(h) ## avancer de h
    left(90)
    forward(l)
    left(90)
    forward(h)
    left(90) #on remet la tortue en orientation initiale !

def deplacer(x, y):
    """x, y des entiers
    deplace la tortue aux coordonnées (x,y) sans tracer de trait, et sans
    changer son oritentation
    """
    penup()
    setpos(x,y)
    pendown()

def rectangle_aa(xd, yd, xf, yf):
    """xd, yd, xf, yf des entiers. xd <= xf, yd <= yf
    Dessine un rectangle ayant pour début (xd, yd) et pour fin (xf, yf)
    """
    deplacer(xd, yd)
    rectangle(xf - xd, yf - yd)

def remplir_rect_aa(xd, yd, xf, yf, couleur):
    """xd, yd, xf, yf des entiers. xd <= xf, yd <= yf, couleur une chaine
    Remplit un rectangle ayant pour début (xd, yd) et pour fin (xf, yf)
    avec la couleur couleur
    """
    color(couleur)
    begin_fill()
    rectangle_aa(xd, yd, xf, yf)
    end_fill()

def dessiner_crewmate(x, y, couleur):
    remplir_rect_aa(x +   0, y +   0, x + 250, y + 400, "black")
    remplir_rect_aa(x + 240, y +  25, x + 330, y + 300, "black")
    remplir_rect_aa(x +  10, y +  10, x + 240, y + 390, couleur)
    remplir_rect_aa(x +   0, y +   0, x + 100, y - 100, "black")
    remplir_rect_aa(x + 150, y +   0, x + 250, y - 100, "black")
    remplir_rect_aa(x +  10, y +  10, x + 90,  y -  90, couleur)
    remplir_rect_aa(x + 160, y +  10, x + 240, y -  90, couleur)
    remplir_rect_aa(x + 250, y +  35, x + 320, y + 290, couleur)
    remplir_rect_aa(x -  15, y + 225, x + 175, y + 325, "black")
    remplir_rect_aa(x -   5, y + 235, x + 165, y + 315, "lightgrey")

if __name__ == "__main__":
    dessiner_crewmate(0,0,"green")

Avec uniquement des fonctions relativement simples, on a donc réussi à créer un programme qui dessine des formes compliquée !

Partie 7 : réflexions

Le corps de la fonction que nous avons vue dans l'exercice précédent est relativement compliqué à lire.

Est-ce un problème si l'on veut utiliser cette fonction pour dessiner plusieurs crewmates ?

L'abstraction y est peut-être pour quelque chose ...

La fonction dessiner_crewmate agit comme une abstraction du code utilisé pour dessiner un crewmate, on peut donc facilement l'utiliser même sans comprendre son code !

Quelque chose manque dans cette fonction pour qu'elle exprime bien le fait qu'elle dessine un crewmate. Proposez et justifiez une amélioration de la fonction, sans forcément la mettre en oeuvre et sans toucher à son corps, pour la rendre plus simple à utiliser.

Ca a peut-être à voir avec la spécification ...

Le nom dessiner_crewmate est beaucoup trop flou : un crewmate peut être beaucoup de choses, d'autant que le lecteur n'aura pas forcément la référence ...

  1. On pourrait préciser la fonction avec une docstring expliquant en détail ce qu'elle dessin. Une telle docstring pourrait faire explicitement référence à Among Us. Il faudrait alors que le lecteur aille voir la docstring de la fonction pour comprendre le code l'utilisant.
  2. On pourraît sinon (ou en plus) modifier le nom de la fonction, par exemple en `dessiner_crewmate_among_us". Le lecteur n'aurait alors plus à aller lire la docstring, mais le nom allongé de la fonction pourrait gêner la lecture du code utilisant la fonction.

En pratique ces deux solutions sont valides, et dépendent beaucoup du contexte, et des conventions de présentation du code choisies.

Partie 8 : réflexion difficile

Le code de la fonction dessiner_crewmate est diffile à lire en partie parce que chaque élément du corps est dessiné deux fois :

- une fois en noir pour faire des contours de type *dessin animé*. 
- une fois en couleur, un peu moins large, pour dessiner l'élément en lui-même.

Imaginons que l'on veuille alléger le code de la fonction dessiner_crewmate, pour le rendre un peu plus simple à lire. Que pourrait-on faire pour simplifier le code ?

Il n'est pas attendu une réponse détaillée, mais une plutôt une idée.

Essayez de voir si vous pouvez "découper" le processus de dessin d'un crewmate en plusieurs étapes, des sous-problèmes, que l'on pourrait donc écrire dans des fonctions séparées.

Remarque : il y a plusieurs découpages possibles.

On a trois possibilités de découpages que l'on peut combiner entre elles :

  • Dessiner d'abord les contours, puis dessiner les éléments. On aurait alors deux fonctions comme dessiner_contours_crewmate et dessiner_couleur_crewmate.
  • Dessiner chaque élément du crewmate l'un après l'autre : jambes, corps, visière, sac à dos. On aurait alors dessiner_jambe_crewmate, dessiner_corps_crewmate, dessiner_visiere_crewmate, dessiner_sac_crewmate
  • Dessiner chaque rectangle avec ses contours. On aurait alors, au lieu de remplir_rect_aa, une fonction remplir_rect_aa_contours.

TP : Jeu des allumettes

Pour illustrer le chapitre, implémentons un jeu des allumettes avec une intelligence artificielle basique. Le jeu des allumettes consiste en un jeu à deux joueurs. On pose n allumettes côte à côte sur une table. Les joueurs tirent entre 1 et 3 allumettes à tour de rôle. Le joueur qui tire la dernière allumette perds la partie2

On va dans un premier temps faire en sorte que le PC tire toujours une et une seule allumette. Une partie ressemblera donc à ça :

Choisissez un nombre d'allumettes pour cette partie : 10

|||||||||| (10)

Combien d'allumettes (1-3)? 3

||||||| (7)

PC : 1

|||||| (6)

Combien d'allumettes (1-3)? 3

||| (3)

PC : 1

|| (2)

Combien d'allumettes (1-3)? 1

| (1)

PC : 1

### victoire du Joueur ###

Partie 1 : sous problèmes

Essayez d'identifier les principaux problèmes (et eventuellement leurs sous problèmes) que nous devons résoudre pour implanter ce jeu.

Essayez de dessiner cette structure de problèmes sur une feuille ou dans un logiciel de dessin, comme drawio, inkscape, ...

Ne passez pas trop de temps sur cette partie, identifier les problèmes dans un programme peut-être difficile. Celà viendra avec la pratique, et c'est en plus du programme de terminale.

On peut aussi voir les problèmes comme des roles ou des responsabilité que doivent remplir les différentes fonctions et autres composants du logiciel :

  • Affichage
  • Calculs
  • ...

On peut identifier quelques problèmes et sous problèmes :

  • Conduire la partie
    • Formatter le plateau du jeu
      • Dessiner des allumettes
    • Gérer la prise de décision du PC
    • Demander son action à l'utilisateur
    • Afficher la victoire
    • Demander le nombre d'allumettes pour la partie
    • Alterner les tours des joueurs

Cette solution n'est pas absolue, de nombreuses autres configurations sont valides. Par exemple, on pourrait considérer que détecter la victoire constitue un sous problème de conduire la partie qui serait digne d'être mis à part.

Si vous êtes confiants, vous pouvez utiliser vos propres problèmes et sous problèmes. A noter que dans ce cas là, vous ne bénéficierez pas des tests automatiques, et vous ne pourrez que vous inspirer des solutions données pour chaque parties.

On va maintenant pour chaque problème implanter une fonction qui le résouds. Dans cette partie, les specifications des fonctions vous sont fournies dans un fichier jeu_des_allumettes.py. Vous pouvez le télécharger en cliquant sur ce lien. Alternativement, si le téléchargement n'est pas supporté par votre navigateur ou bloqué par votre établissement, pouvez le copier à partir du dépliant ci-dessous :

Code de base du TP
jeu_des_allumettes.py
lancer_tests_automatiques = False ##Mettez à False pour ne pas lancer les tests
lancer_tests_interface = False ##Mettez à True pour lancer les tests semi-automatiques
afficher_traces = False ##Mettez a True pour afficher la trace complète des erreurs dans les tests

def allumettes(n):
    """
    n un entier > 0
    retourne une chaîne composée de n barres | 

    allumettes(5) vaut '|||||'
    """
    pass

def plateau(n):
    """
    n un entier > 0
    retourne une chaîne composée de n barres | , suivi de n entre ()

    plateau(5) vaut `||||| (5)`
    """
    pass

def coup_basique_pc(a):
    """
    a un entier > 0, représente le nombre d'allumettes
    retourne toujours 1

    coup_basique_pc(10000) vaut 1
    """
    pass

def autre_joueur(joueur):
    """
    joueur une chaine qui contient de nom "Joueur" ou "PC".
    retourne l'autre chaine

    autre_joueur("Joueur") vaut "PC"
    """
    pass

def afficher_victoire(joueur):
    """
    joueur une chaine qui contient le nom ("Joueur" ou "PC") du vainqueur.
    Affiche une chaîne qui annonce la victoire de ce joueur !
    """
    pass  

def demander_coup_joueur():
    """
    Demande au joueur combien d'allumettes il veut retirer (entier de 1 a 3)
    retourne la valeur saisie par le joueur (type int).
    """
    pass

def derouler_tour(joueur, al):
    """
    joueur une chaine qui contient le nom ("Joueur" ou "PC") du joueur en cours.
    al un entier, le nombre d'allumettes au debut du tour

    Affiche le nombre d'allumettes, s'enquiert du coup du joueur en cours,
    puis retourne le nouveau nombre d'allumettes
    """
    pass


############################################
#############   PARTIE TESTS   ############# 
############################################

import unittest
if not afficher_traces:
    __unittest = True

def checktype(expr, res, exp):
    assert type(res) == exp, expr + " doit retourner une valeur de type " + exp.__name__ + ", mais a retourné une valeur de type " + type(res).__name__

def checkval_equal(expr, res, exp):
    str_exp = "'" +exp+ "'" if type(exp) == str else str(exp) 
    str_res = "'" +res+ "'" if type(res) == str else str(res) 
    assert res == exp, expr + " doit retourner " + str_exp + ", mais a retourné " + str_res

class Test_allumettes(unittest.TestCase):

    def test_1(self):
        res = allumettes(1)        
        checktype("allumettes(1)", res, str)
        checkval_equal("allumettes(1)", res, "|")

    def test_10(self):
        res = allumettes(10)
        checktype("allumettes(10)", res, str)
        checkval_equal("allumettes(10)", res, "||||||||||")

class Test_plateau(unittest.TestCase):

    def test_1(self):
        res = plateau(1)        
        checktype("plateau(1)", res, str)
        checkval_equal("plateau(1)", res, "| (1)")

    def test_10(self):
        res = plateau(10)
        checktype("plateau(10)", res, str)
        checkval_equal("plateau(10)", res, "|||||||||| (10)")

class Test_coup_basique_pc(unittest.TestCase):

    def test_1(self):
        res = coup_basique_pc(1)        
        checktype("coup_basique_pc(1)", res, int)
        checkval_equal("coup_basique_pc(1)", res, 1)

    def test_10(self):
        res = coup_basique_pc(10)
        checktype("coup_basique_pc(10)", res, int)
        checkval_equal("coup_basique_pc(10)", res, 1)

    def test_10934(self):
        res = coup_basique_pc(10934)
        checktype("coup_basique_pc(10934)", res, int)
        checkval_equal("coup_basique_pc(10934)", res, 1)     

class Test_autre_joueur(unittest.TestCase):

    def test_Joueur(self):
        res = autre_joueur("Joueur")        
        checktype("autre_joueur('Joueur')", res, str)
        checkval_equal("autre_joueur('Joueur')", res, "PC")


    def test_PC(self):
        res = autre_joueur("PC")        
        checktype("autre_joueur('PC')", res, str)
        checkval_equal("autre_joueur('PC')", res, "Joueur")

if __name__ == '__main__' and lancer_tests_automatiques:
    s = unittest.TestSuite()
    s.addTests(unittest.TestLoader().loadTestsFromTestCase(Test_allumettes))
    s.addTests(unittest.TestLoader().loadTestsFromTestCase(Test_plateau))
    s.addTests(unittest.TestLoader().loadTestsFromTestCase(Test_coup_basique_pc))
    s.addTests(unittest.TestLoader().loadTestsFromTestCase(Test_autre_joueur))
    unittest.TextTestRunner().run(s)


def check_interface_afficher_victoire():
    print("\nTEST INTERFACE : afficher_victoire")
    afficher_victoire("PC")
    res = input("TEST INTERFACE : la victoire du PC est-elle correctement affichée (oui/non) ? ")
    if(res != "oui"):
        assert False, "ECHEC"

def check_interface_demander_coup_joueur():
    print("\nTEST INTERFACE : demander_coup_joueur")
    print("TEST INTERFACE : A la prochaine question, répondez 3 !")
    res = demander_coup_joueur()
    checktype("demander_coup_joueur()", res, int)
    checkval_equal("demander_coup_joueur()", res, 3)
    res = input("TEST INTERFACE : la question affichait que la réponse était entre 1 et 3 (oui/non) ? ")
    if(res != "oui"):
        assert False, "ECHEC"

def check_interface_derouler_tour_PC():
    print("\nTEST INTERFACE : derouler_tour('PC')")
    r = derouler_tour("PC", 4)
    checktype("derouler_tour('PC', 4)", r, int)
    checkval_equal("derouler_tour('PC', 4)", r, 3)
    res = input("TEST INTERFACE : C'est le PC qui a joué (oui/non) ? ")
    if(res != "oui"):
        assert False, "ECHEC"    
    res = input("TEST INTERFACE : Il y avait 4 allumettes à la base (oui/non) ? ")
    if(res != "oui"):
        assert False, "ECHEC"
    res = input("TEST INTERFACE : Il a retiré 1 allumette, celà a été affiché (oui/non) ? ")
    if(res != "oui"):
        assert False, "ECHEC"

def check_interface_derouler_tour_Joueur():
    print("\nTEST INTERFACE : derouler_tour('Joueur')")
    print("TEST INTERFACE : A la prochaine question, répondez 2 !")
    r = derouler_tour("Joueur", 5)
    checktype("derouler_tour('Joueur', 5)", r, int)
    checkval_equal("derouler_tour('Joueur', 5)", r, 3)
    res = input("TEST INTERFACE : C'est le Joueur qui a joué (oui/non) ? ")
    if(res != "oui"):
        assert False, "ECHEC" 
    res = input("TEST INTERFACE : Il y avait 5 allumettes à la base (oui/non) ? ")
    if(res != "oui"):
        assert False, "ECHEC"

if __name__ == '__main__' and lancer_tests_interface:
    print("LES TESTS D'INTERFACE COMMENCENT")
    check_interface_afficher_victoire() 
    check_interface_demander_coup_joueur()
    check_interface_derouler_tour_PC()
    check_interface_derouler_tour_Joueur()
    print("\nSUCCES")

Partie 2 : fonctions pures

En premier lieu, on va implanter les fonctions pures, qui peuvent être testées automatiquement.

Implantez les fonctions `suivantes à partir de leurs spécifications :

  • allumettes (1 ligne)
  • plateau (1 ligne)
  • coup_basique_pc (1 ligne)
  • autre joueur (4 lignes)

Votre code peut être différent de ces solutions. L'important est que les test automatiquent passent.

def allumettes(n):
    """
    n un entier > 0
    retourne une chaîne composée de n barres | 

    allumettes(5) vaut '|||||'
    """
    return "|" * n

def plateau(n):
    """
    n un entier > 0
    retourne une chaîne composée de n barres | , suivi de n entre ()

    plateau(5) vaut `||||| (5)`
    """
    return allumettes(n) + " (" + str(n) + ")"

def coup_basique_pc(a):
    """
    a un entier > 0, représente le nombre d'allumettes
    retourne toujours 1

    coup_basique_pc(10000) vaut 1
    """
    return 1

def autre_joueur(joueur):
    """
    joueur une chaine qui contient de nom "Joueur" ou "PC".
    retourne l'autre chaine

    autre_joueur("Joueur") vaut "PC"
    """
    if joueur == "PC":
        return "Joueur"
    else :
        return "PC"

Les fonctions d'interface, qui demandent des entrées et sorties à l'utilisateur sont plus difficiles à tester automatiquement. On se contentera donc de tests semi-automatiques, dans lesquels le programme vous guidera dans les vérifications à effectuer. Vous pouvez activer ces tests en mettant lancer_tests_interface à True.

Une bonne pratique en programmation est d'essayer d'"isoler" les entrées et sorties du reste du code, et de privilégier les fonctions pures. C'est pour ça que allumettes et plateau retournent une valeur au lieu d'afficher directement.

Partie 3 : fonctions d'interface

Implantez les fonction d'interfaces suivantes en considérant que l'utilisateur ne fera pas d'erreur de saisie. Respectez l'ordre d'implémentation, les tests bloquent à la première fonction qui ne marche pas !

  • demander_coup_joueur (1 ligne)
  • afficher_victoire (2 lignes)
  • derouler_tour (10 lignes)

On considèrera que l'utilisateur ne fait pas d'erreur de saisie.

Les deux premières fonctions sont normalement simples à implémenter.

La troisième vous demandera - d'utiliser une structure if else pour choisir si le joueur ou le PC joue.
- d'appeler la fonction qui corresponds - demander_coup_joueur pour le joueur - coup_basique_pc pour le PC

Votre code sera probablement différent. Tant que les tests d'interface, effectués avec grande attention, passent, c'est normalement bon.

def afficher_victoire(joueur):
    """
    joueur une chaine qui contient le nom ("Joueur" ou "PC") du vainqueur.
    Affiche une chaîne qui annonce la victoire de ce joueur !
    """
    print("\n### Victoire du " + joueur + " ###")    

def demander_coup_joueur():
    """
    Demande au joueur combien d'allumettes il veut retirer (entier de 1 a 3)
    retourne la valeur saisie par le joueur (type int).
    """
    coup = input("Combien d'allumettes (1-3)? ") 
    return int(coup)

def derouler_tour(joueur, al):
    """
    joueur une chaine qui contient le nom ("Joueur" ou "PC") du joueur en cours.
    al une chaine qui contient le nombre d'allumettes au debut du tour

    Affiche le nombre d'allumettes, s'enquiert du coup du joueur en cours,
    puis retourne le nouveau nombre d'allumettes
    """
    print() #les prints vides sont là pour sauter une lignes de formattage
    print(plateau(al))
    if joueur == "Joueur":
        print()
        coup = demander_coup_joueur()
    else:
        coup = coup_basique_pc(al)
        print() 
        print("PC : " + str(coup))
    return al - coup

Partie 4 : test système

Jusqu'à maintenant, nos tests ont testé une seule fonction à la fois. Ce sont des tests unitaires. On est donc presque assurés que les fonctions marchent bien.

Mais ce n'est pas suffisant, il nous faut aussi tester notre programme tout entier. Pour ça, on va faire un test à la main, mais pour être rigoureux, on va écrire le scénario du test quelque part, comme ça on pourra le suivre à la lettre quand on testera.

Proposez un tel scénario, il doit comporter :

  • chaque action que le testeur doit faire à chaque étape de l'execution du programme qui demande une action (par exemple, la valeur du nombre à entrer)
  • A chaque étape, le comportement attendu du programme, et les points importants que le testeur doit véfifier.

Ecrivez votre scénario lisiblement sur une feuille ou dans un fichier txt ou markdown.

Remarque : on suppose que l'utilisateur ne peut pas se tromper en faisant une saisie, sinon celà pourrait énormément compliquer le scénario.

Vous pouvez partir de la trace d'execution qui a été présentée juste avant la partie 1, et rajouter des éléments entre [[]] pour la préciser.

[[un element]]

Remarque : il y a peut-être des points supplémentaires sur lequel il faut faire attention.

[[Scénario de test du jeu des allumettes]]
[[A la fin de ce scénario, le programme doit terminer sans erreur.]]

[[Action testeur : entrer 10]]        
Choisissez un nombre d'allumettes pour cette partie : 10

|||||||||| (10)
[[Vérifier qu'il y a bien 10 allumettes]]

[[Action testeur : entrer 3]]
Combien d'allumettes (1-3)? 3
[[Vérifier que le nombre d'allumettes autorisées est bien affiché]]

||||||| (7)

PC : 1
[[Vérifier que le PC a bien tiré une allumette]]

|||||| (6)

[[Action testeur : entrer 3]]
Combien d'allumettes (1-3)? 3

||| (3)

PC : 1

|| (2)

[[Action testeur : entrer 1]]
Combien d'allumettes (1-3)? 1

| (1)

PC : 1

### victoire du Joueur ###
[[Vérifier que c'est bien le Joueur et non le PC qui est victorieux]]

Partie 5 : fonction main

Implantez la fonction main et un bloc if pour la lancer, comme vu précédemment dans le cours.

La fonction main doit demander un nombre d'allumettes initial au joueur, désigner le joueur comme celui qui commence, puis enchainer les tours tant qu'il reste des allumettes, et finalement afficher le joueur victorieux.

Utilisez votre scénario de test pour tester votre programme.

Il y a probablement une boucle while quelque part.

Utilisez les fonctions que l'on a déjà implantées !

N'hésitez pas à mettre des commentaires.

def main():
    """
    demande un nombre d'allumettes au joueur, puis lance la partie avec ce nombre d'allumettes,
    jusqu'à qu'un joueur gagne. Ensuite, affiche la victoire.
    """
    al = int(input("Choisissez un nombre d'allumettes pour cette partie : "))
    joueur = "Joueur"

    while al > 0:
        al = derouler_tour(joueur, al)
        joueur = autre_joueur(joueur)

    afficher_victoire(joueur)


if __name__=="__main__":
    main()

La solution complète peut être téléchargée en cliquant sur ce lien, ou alternativement par le biais du dépliant :

Solution complète du TP
lancer_tests_automatiques = False ##Mettez à False pour ne pas lancer les tests
lancer_tests_interface = False ##Mettez à True pour lancer les tests semi-automatiques
afficher_traces = False ##Mettez a True pour afficher la trace complète des erreurs dans les tests

def allumettes(n):
    """
    n un entier > 0
    retourne une chaîne composée de n barres | 

    allumettes(5) vaut '|||||'
    """
    return "|" * n

def plateau(n):
    """
    n un entier > 0
    retourne une chaîne composée de n barres | , suivi de n entre ()

    plateau(5) vaut `||||| (5)`
    """
    return allumettes(n) + " (" + str(n) + ")"

def coup_basique_pc(a):
    """
    a un entier > 0, représente le nombre d'allumettes
    retourne toujours 1

    coup_basique_pc(10000) vaut 1
    """
    return 1

def autre_joueur(joueur):
    """
    joueur une chaine qui contient de nom "Joueur" ou "PC".
    retourne l'autre chaine

    autre_joueur("Joueur") vaut "PC"
    """
    if joueur == "PC":
        return "Joueur"
    else :
        return "PC"

def afficher_victoire(joueur):
    """
    joueur une chaine qui contient le nom ("Joueur" ou "PC") du vainqueur.
    Affiche une chaîne qui annonce la victoire de ce joueur !
    """
    print("\n### VICTOIRE DU " + joueur + " ###")    

def demander_coup_joueur():
    """
    Demande au joueur combien d'allumettes il veut retirer (entier de 1 a 3)
    retourne la valeur saisie par le joueur (type int).
    """
    coup = input("Combien d'allumettes (1-3)? ") 
    return int(coup)

def derouler_tour(joueur, al):
    """
    joueur une chaine qui contient le nom ("Joueur" ou "PC") du joueur en cours.
    al un entier, le nombre d'allumettes au debut du tour

    Affiche le nombre d'allumettes, s'enquiert du coup du joueur en cours,
    puis retourne le nouveau nombre d'allumettes
    """
    print() #les prints vides sont là pour sauter une lignes de formattage
    print(plateau(al))
    if joueur == "Joueur":
        print()
        coup = demander_coup_joueur()
    else:
        coup = coup_basique_pc(al)
        print() 
        print("PC : " + str(coup))
    return al - coup

def main():
    """
    demande un nombre d'allumettes au joueur, puis lance la partie avec ce nombre d'allumettes,
    jusqu'à qu'un joueur gagne. Ensuite, affiche la victoire.
    """
    al = int(input("Choisissez un nombre d'allumettes pour cette partie : "))
    joueur = "Joueur"

    while al > 0:
        al = derouler_tour(joueur, al)
        joueur = autre_joueur(joueur)

    afficher_victoire(joueur)


if __name__=="__main__":
    main()

############################################
#############   PARTIE TESTS   ############# 
############################################

import unittest
if not afficher_traces:
    __unittest = True

def checktype(expr, res, exp):
    assert type(res) == exp, expr + " doit retourner une valeur de type " + exp.__name__ + ", mais a retourné une valeur de type " + type(res).__name__

def checkval_equal(expr, res, exp):
    str_exp = "'" +exp+ "'" if type(exp) == str else str(exp) 
    str_res = "'" +res+ "'" if type(res) == str else str(res) 
    assert res == exp, expr + " doit retourner " + str_exp + ", mais a retourné " + str_res

class Test_allumettes(unittest.TestCase):

    def test_1(self):
        res = allumettes(1)        
        checktype("allumettes(1)", res, str)
        checkval_equal("allumettes(1)", res, "|")

    def test_10(self):
        res = allumettes(10)
        checktype("allumettes(10)", res, str)
        checkval_equal("allumettes(10)", res, "||||||||||")

class Test_plateau(unittest.TestCase):

    def test_1(self):
        res = plateau(1)        
        checktype("plateau(1)", res, str)
        checkval_equal("plateau(1)", res, "| (1)")

    def test_10(self):
        res = plateau(10)
        checktype("plateau(10)", res, str)
        checkval_equal("plateau(10)", res, "|||||||||| (10)")

class Test_coup_basique_pc(unittest.TestCase):

    def test_1(self):
        res = coup_basique_pc(1)        
        checktype("coup_basique_pc(1)", res, int)
        checkval_equal("coup_basique_pc(1)", res, 1)

    def test_10(self):
        res = coup_basique_pc(10)
        checktype("coup_basique_pc(10)", res, int)
        checkval_equal("coup_basique_pc(10)", res, 1)

    def test_10934(self):
        res = coup_basique_pc(10934)
        checktype("coup_basique_pc(10934)", res, int)
        checkval_equal("coup_basique_pc(10934)", res, 1)     

class Test_autre_joueur(unittest.TestCase):

    def test_Joueur(self):
        res = autre_joueur("Joueur")        
        checktype("autre_joueur('Joueur')", res, str)
        checkval_equal("autre_joueur('Joueur')", res, "PC")


    def test_PC(self):
        res = autre_joueur("PC")        
        checktype("autre_joueur('PC')", res, str)
        checkval_equal("autre_joueur('PC')", res, "Joueur")

if __name__ == '__main__' and lancer_tests_automatiques:
    s = unittest.TestSuite()
    s.addTests(unittest.TestLoader().loadTestsFromTestCase(Test_allumettes))
    s.addTests(unittest.TestLoader().loadTestsFromTestCase(Test_plateau))
    s.addTests(unittest.TestLoader().loadTestsFromTestCase(Test_coup_basique_pc))
    s.addTests(unittest.TestLoader().loadTestsFromTestCase(Test_autre_joueur))
    unittest.TextTestRunner().run(s)


def check_interface_afficher_victoire():
    print("\nTEST INTERFACE : afficher_victoire")
    afficher_victoire("PC")
    res = input("TEST INTERFACE : la victoire du PC est-elle correctement affichée (oui/non) ? ")
    if(res != "oui"):
        assert False, "ECHEC"

def check_interface_demander_coup_joueur():
    print("\nTEST INTERFACE : demander_coup_joueur")
    print("TEST INTERFACE : A la prochaine question, répondez 3 !")
    res = demander_coup_joueur()
    checktype("demander_coup_joueur()", res, int)
    checkval_equal("demander_coup_joueur()", res, 3)
    res = input("TEST INTERFACE : la question affichait que la réponse était entre 1 et 3 (oui/non) ? ")
    if(res != "oui"):
        assert False, "ECHEC"

def check_interface_derouler_tour_PC():
    print("\nTEST INTERFACE : derouler_tour('PC')")
    r = derouler_tour("PC", 4)
    checktype("derouler_tour('PC', 4)", r, int)
    checkval_equal("derouler_tour('PC', 4)", r, 3)
    res = input("TEST INTERFACE : C'est le PC qui a joué (oui/non) ? ")
    if(res != "oui"):
        assert False, "ECHEC"    
    res = input("TEST INTERFACE : Il y avait 4 allumettes à la base (oui/non) ? ")
    if(res != "oui"):
        assert False, "ECHEC"
    res = input("TEST INTERFACE : Il a retiré 1 allumette, celà a été affiché (oui/non) ? ")
    if(res != "oui"):
        assert False, "ECHEC"

def check_interface_derouler_tour_Joueur():
    print("\nTEST INTERFACE : derouler_tour('Joueur')")
    print("TEST INTERFACE : A la prochaine question, répondez 2 !")
    r = derouler_tour("Joueur", 5)
    checktype("derouler_tour('Joueur', 5)", r, int)
    checkval_equal("derouler_tour('Joueur', 5)", r, 3)
    res = input("TEST INTERFACE : C'est le Joueur qui a joué (oui/non) ? ")
    if(res != "oui"):
        assert False, "ECHEC" 
    res = input("TEST INTERFACE : Il y avait 5 allumettes à la base (oui/non) ? ")
    if(res != "oui"):
        assert False, "ECHEC"

if __name__ == '__main__' and lancer_tests_interface:
    print("LES TESTS D'INTERFACE COMMENCENT")
    check_interface_afficher_victoire() 
    check_interface_demander_coup_joueur()
    check_interface_derouler_tour_PC()
    check_interface_derouler_tour_Joueur()
    print("\nSUCCES")

Partie 6 : ouverture

Pour cette partie, vous pouvez repartir du code fourni dans le dépliant juste au dessus. Il ne vous est pas demandé de répondre forcément juste (la plupart des questions n'ont pas qu'une seule "bonne réponse"), mais de rendre compte de votre démarche et de votre réflexion !

Donnez une représentation grapique du programme, qui montre : Les fonction et les appels qu'elles font à d'autres fonctions du programme.

On veut ameliorer la stratégie du PC. Combien de fonctions faut-il modifier au minimum ? Expliquez en quelques lignes votre raisonnement.. Remarque : vous avez le droit de partir du code de la solution complète.

Proposez une idée pour améliorer la stratégie du PC. Expliquez comment vous l'avez trouvée (raisonnement, recherche sur Internet, utilisation d'un module ou d'une bibliothèque comme le module random, ...).

Implantez votre stratégie dans le programme, ou expliquez comment vous l'implanteriez si vous ne savez pas exactement comment faire : la structure du problème, du programme (les fonctions et leurs spécifications).

Les essentiels

--8<-- "architecture_fonctions/fiche.md


  1. Ce jeu est bien entendu Among Us, développé par Innersloth : https://www.innersloth.com/games/among-us/. Attention toutefois, ce n'est pas un logiciel libre ! 

  2. Ou l'inverse, celui qui tire la dernière gagne, mais le principe est le même.