Fonctions, assertions et docstring en Python
Premiers pas avec les fonctions
Pourquoi des fonctions ?
Dans la partie précédente, nous avons terminé par un petit programme qui demande à un·e utilisateur·trice de donner un nombre entier entre 1 et 10, et qui vérifie la saisie jusqu'à ce que l'utilisateur·trice ait effectué une saisie correcte. Voici un exemple simple d'implémentation de ce programme, tel que celui que vous avez proposé :
saisie = input("Entrez un nombre entre 1 et 10 :")
while not(saisie in [ "1", "2", "3","4", "5", "6", "7", "8", "9", "10" ]) :
saisie = input("Entrez un nombre entre 1 et 10 :")
nb = int(saisie)
print(f"Vous avez saisi {nb}")
Un blocage ?
Quels sont les problèmes pouvant être posé par ce programme ?
-
En premier lieu, les lignes 1 et 3 sont des répétitions (des copiés-collés). Or en informatique, on essaye le plus possible de respecter le principe DRY : Don't Repeat Yourself. Une méthode pour corriger ce point pourrait être la suivante :
repeter = True while repeter : saisie = input("Entrez un nombre entre 1 et 10 : ") if saisie in [ "1", "2", "3","4", "5", "6", "7", "8", "9", "10" ] : nb = int(saisie) repeter = False print(f"Vous avez saisi {nb}") -
Dans un second temps, le code fonctionne dans ce cas précis, mais n'est pas adaptable à d'autres situations. En effet, on doit saisir dans la liste toutes les possibilités pouvant être saisies, ce qui rend le code peu adaptable à une situation du style « Saisir un nombre entier entre -1000 et 1000 ». Il faut donc modifier complètement la logique de ce programme. Entre autres, il faut découper le code en plusieurs morceaux :
- On commencera d'abord à tester si quelque chose a bien été saisi, sinon il faut reposer la question.
- Il faut ensuite tester si la suite de caractères saisie correspond bien aux caractères des entiers, et rien qu'à ceux-ci. On pourrait aussi tester si le premier caractère est un signe « - », pour prendre en compte les entiers négatifs.
- Enfin, une fois certain que la saisie est bien un entier, on la trans-type, puis on regarde si la condition d'encadrement est correcte.
Le code suivant donne toutes ces étapes :
repeter = True # Booléen servant à exécuter la boucle while repeter : # Tant que repeter est vrai saisie = input("Entrez un nombre entre 1 et 10 : ") if saisie != "" : # On ne travaille que si la saisie est une chaine non vide ######## ON TESTE SI LA SAISIE EST CELLE D'UN ENTIER ####### est_un_entier = True # On suppose que c'est un entier indice_depart = 0 # et que le premier caractère est un chiffre if saisie[0] == "-" :# si le premier caractère est un signe "-", indice_depart = 1# On regardera chaque caractère à partir de l'indice 1 for caractere in saisie[indice_depart : len(saisie)] : # Pour chaque caractère if caractere not in "0123456789" : # Si ce n'est pas un chiffre est_un_entier = False # Alors la saisie n'est pas un entier ###### ON TESTE SI IL EST ENTRE 1 et 10 ####### if est_un_entier : # nb = int(saisie)# Trans-typage if 1<= nb <= 10 : # Si les conditions sont remplies repeter = False # On arrête la boucle print(f"Vous avez saisi {nb}") # Juste là pour montrer la saisie
Application 1 : Multiplier les codes
Imaginons maintenant un programme qui demande trois fois à l'utilisateur·trice d'entrer un tel nombre, puis qui vérifie si les trois nombres correspondent à une configuration de Pythagore (c'est-à-dire \(a^2 + b^2 = c^2\)).
Vous devez vous inspirer du programme précédent pour compléter le programme ci-dessous afin d'obtenir trois nombres entiers nb1, nb2 et nb3 saisis par l'utilisateur·trice :
Un programme avec notre niveau actuel en Python pourrait être le suivant :
repeter = True
while repeter :
saisie1 = input("Entrez un nombre entre 1 et 10 : ")
if saisie1 != "" :
est_un_entier = True
indice_depart = 0
if saisie1[0] == "-" :
indice_depart = 1
for caractere in saisie1[indice_depart : len(saisie1)] :
if caractere not in "0123456789" :
est_un_entier = False
if est_un_entier :
nb1 = int(saisie1)
if 1<= nb1 <= 10 :
repeter = False
repeter = True
while repeter :
saisie2 = input("Entrez un nombre entre 1 et 10 : ")
if saisie2 != "" :
est_un_entier = True
indice_depart = 0
if saisie2[0] == "-" :
indice_depart = 1
for caractere in saisie2[indice_depart : len(saisie2)] :
if caractere not in "0123456789" :
est_un_entier = False
if est_un_entier :
nb2 = int(saisie2)
if 1<= nb2 <= 10 :
repeter = False
repeter = True
while repeter :
saisie3 = input("Entrez un nombre entre 1 et 10 : ")
if saisie3 != "" :
est_un_entier = True
indice_depart = 0
if saisie3[0] == "-" :
indice_depart = 1
for caractere in saisie3[indice_depart : len(saisie3)] :
if caractere not in "0123456789" :
est_un_entier = False
if est_un_entier :
nb3 = int(saisie3)
if 1<= nb3 <= 10 :
repeter = False
if nb1**2 + nb2**2 == nb3**2 or nb2**2 + nb3**2 == nb1**2 or nb3**2 + nb1**2 == nb2**2 :
print(f"({nb1}, {nb2}, {nb3}) est un triplet de Pythagore")
else :
print(f"({nb1}, {nb2}, {nb3}) n'est pas un triplet de Pythagore)
Mauvaise pratique
Si ce code fait le travail demandé, il n'en est pas moins de mauvaise qualité pour plusieurs raisons :
- Il est long, avec des répétitions. Plus un code est long, plus il prend de place en mémoire, alors qu'on cherche plutôt à minimiser ce facteur. Et surtout, plus un code est long, moins il est lisible !
- Le copié-collé force quand même à repasser sur chacune des parties pour modifier le nom des variables (sinon les valeurs seraient écrasées). Ainsi toute modification dans la structure du code doit être réécrite à chaque fois que le morceau de code est copié. Dans notre exemple, ce n'est que trois fois, le risque d'erreurs est minime, mais pour un plus grand nombre de répétitions, il est très facile d'oublier une modification.
Première fonction
Définition d'une fonction
On va donc améliorer non seulement la lisibilité de notre code, mais aussi son efficacité et sa simplicité, en utilisant une fonction.
Définition : fonction
Une fonction est un bloc de code nommé (c'est-à-dire possédant un nom dans l'espace de noms, comme toute autre variable) et qui :
- n'est pas exécuté au moment de sa définition ;
- mais peut être appelé plusieurs fois dans le code par l'intermédiaire de son nom, suivi d'une paire de parenthèses. Ces appels (call en anglais) exécuteront alors l'intégralité du code à chaque appel, et renverra une valeur de retour, c'est-à-dire un objet qui pourra être utilisé normalement dans la partie de code ayant appelé la fonction.
Une fonction est crée en Python par le mot clé def suivi du nom de la fonction et des paramètres utilisés, puis d'un caractère :, et dans le bloc de code la ou les valeurs renvoyées sont spécifiées avec le mot clé return :
def est_entier_et_strictement_positif(nb : int | float) -> bool :
if isinstance(nb)== int and nb > 0 :
return True
else :
return False
En regardant la première ligne, qu'on appelle la signature de la fonction :
def est_entier_et_strictement_positif(nb : int | float) -> bool :
- La fonction est introduite par le mot clé
def, suivi du nom de la fonction puis d'un couple de parenthèses(),contenant le paramètrenb, dont on précise qu'il doit être de typeintoufloat, grâce aux type hints de Python, en français indices de type. Les type hints ne sont que des indications en Python, mais ils sont impératifs dans d'autres langages (C, C++, Rust, etc...) - la notation
-> boolest un type hint, qui indique que la valeur renvoyée par la fonction sera de typebool. - Les deux points définissent un bloc de code qui est repéré par une indentation, tout comme on définit des blocs dans des structures conditionnelles ou des boucles.
- la fonction renvoie un objet de type
bool, selon les conditions vérifiées par la structure conditionnelle.
La fonction peut ensuite être appelée dans le terminal :
>>> est_entier_et_strictement_positif(54)
True
>>> est_entier_et_strictement_positif(-3)
False
>>> est_entier_et_strictement_positif(5.4)
False
Quelques exemples :
def f(x : float ) -> float :
return 3*x +4
- possède un paramètre
xde typefloat; - renvoie l'image de
xpar loa fonction \(f : x \mapsto 3x+4\).
Une fois définie, on l'utilise de la manière suivante (dans le terminal) :
>>> f(5)
19
>>> f(2.3)
10.9
def formate_nom(nom : str, prenom : str)-> str :
nomf = nom.upper()
prenomf = prenom.capitalize()
return nomf+" "+prenomf
- possède deux paramètres
nometprenomqui sont deux chaines de caractères ; - utilise deux variables locales
nomfetprenomf; - renvoie une chaine de caractère formatée contenant le nom en majuscule puis le prénom en minuscule, avec la première lettre en majuscule.
def ask_user_int() -> int:
repeter = True
while repeter :
saisie = input("Entrez un nombre entre 1 et 10 : ")
if saisie != "" :
est_un_entier = True
indice_depart = 0
if saisie[0] == "-" :
indice_depart = 1
for caractere in saisie[indice_depart : len(saisie)] :
if caractere not in "0123456789" :
est_un_entier = False
if est_un_entier :
nb = int(saisie)
if 1<= nb <= 10 :
repeter = False
return nb
Oubli des parenthèses
Dans le cas d'un oubli des parenthèses lorsqu'on appelle une fonction, on obtient dans le terminal le message suivant :
>>> ask_user_int
<function ask_user_int at 0xe8c1a0>
Qui signifie simplement que le nom ask_user_int fait référence à une fonction dont l'adresse dans la mémoire de l'ordinateur est donnée sous forme hexadécimale.
Pour les cracks
Notez que ask_user_int est juste un nom, l'objet correspondant est stocké dans l'espace des objets. On peut donc écrire les choses suivantes :
>>> demande_entier=ask_user_int
>>> demande_entier()
Utilisation de la valeur de retour
Comme tout objet, la valeur de retour d'une fonction doit elle-même être stockée dans une variable afin de ne pas être ramassée par le garbage collector.
Exemple
>>> entier = ask_user_int()
>>> print(f'La racine carrée du nombre {entier} est {entier**(1/2)}')
L'interpréteur Python évalue la ligne entier = ask_user_int() :
- Il commence par appeler le code correspondant à La fonction
ask_user_int(). - Le code de la fonction demande une saisie à l'utilisateur, et une fois l'instruction
returnatteinte, l'instructionask_user_int()est remplacée à l'endroit de l'appel par la valeur saisie par l'utilisateur, converti en objet de type int. - L'objet est ensuite stocké dans une variable nommée
entier, et peut alors être utilisé en dehors de la fonction.
Factorisation du code de Pythagore
Le code du programme de vérification de Pythagore peut alors être factorisé (ce qui signifie globalement qu'on réduit sa taille en évitant les répétitions), afin de respecter le principe DRY :
#### Zone des fonctions
def ask_user_int() -> int:
repeter = True
while repeter :
saisie = input("Entrez un nombre entre 1 et 10 : ")
if saisie != "" :
est_un_entier = True
indice_depart = 0
if saisie[0] == "-" :
indice_depart = 1
for caractere in saisie[indice_depart : len(saisie)] :
if caractere not in "0123456789" :
est_un_entier = False
if est_un_entier :
nb = int(saisie)
if 1<= nb <= 10 :
repeter = False
return nb
### Code principal
nb1 = ask_user_int()
nb2 = ask_user_int()
nb3 = ask_user_int()
if (nb1**2 == nb2**2 + nb3**2) or (nb2**2 == nb1**2 + nb3**2) or (nb3**2 == nb2**2 + nb1**2) :
print("C'est une configuration de Pythagore !")
else :
print("Ce n'est pas une configuration de Pythagore !")
Ce qui a l'avantage d'être vraiment plus clair.
Exercices
Application 2
Créer une fonction nommée table7 qui renvoie la table de multiplication de 7 avec un multiplicateur allant de 0 à 10, sous la forme d'une chaîne de caractères comme ci-dessous :
"7x0=0 \n 7x1=7 \n 7x2=14 ..."
Indication : le symbole \n, insère un saut de ligne dans une chaîne de caractères.
Attention : l'énoncé ne demande pas d'imprimer, mais bien de renvoyer un objet de type chaine de caractère.
def table7() -> str:
table = ''
for i in range(11) :
table += f'7x{i} = {7*i} \n'
return table
Augmenter la capacité des fonctions : les paramètres obligatoires
Premiers
L'exemple de la fonction ask_user_int est assez limité. Dans l'absolu, on pourrait souhaiter que la fonction demande un nombre entier entre 2 valeurs variables, par exemple entre 1 et 100 ou bien entre -10 et 10.
Pour ce faire, il faut, dans la définition de la fonction, préciser des paramètres qui seront des objets transmis et nommés initialisés à certaines valeurs lors de l'appel à la fonction :
Exemple :
def ask_user_int(borne_min : int, borne_max : int) -> int:
repeter = True
while repeter :
saisie = input(f"Entrez un nombre entre {borne_min} et {borne_max} : ")
if saisie != "" :
est_un_entier = True
indice_depart = 0
if saisie[0] == "-" :
indice_depart = 1
for caractere in saisie[indice_depart : len(saisie)] :
if caractere not in "0123456789" :
est_un_entier = False
if est_un_entier :
nb = int(saisie)
if borne_min<= nb <= borne_max :
repeter = False
return nb
La fonction ask_user_int utilise maintenant deux paramètres borne_min et borne_max, dont les type hints indiquent que ce doit être deux entiers.
Une fois la fonction définie, on peut l'appeler en lui passant des arguments, c'est-à-dire des valeurs qui seront affectées aux paramètres :
>>> ask_user_int(1,100)
>>> ask_user_int(-10,10)
Erreurs d'appels et notion de précondition
Avec le code actuel de la fonction ask_user_int, on peut rencontrer un certain nombre de difficultés. Testez les commandes suivantes dans la console
ask_user_int()
L'erreur signalée est de type TypeError, et le message d'erreur nous dit qu'il manque 2 arguments lors de l'appel, et que ces arguments sont borne_min et borne_max.
ask_user_int(45)
L'erreur signalée est de type TypeError, et le message d'erreur nous dit qu'il manque 1 argument lors de l'appel, et que cet argument est borne_max. Cela signifie aussi que le nom borne_min a bien reçu la valeur 45.
ask_user_int(0.5,2.5)
Aucun véritable erreur n'est levée par Python, bien que les type hints demandent des arguments de type int. Comme dit précédemment, ces indications sont facultatives (à destination des programmeur·euse·s), et ici les transgresser ne pose pas vraiment de problème.
ask_user_int(30,10)
Ici, la fonction tourne en boucle et il est impossible de saisir une réponse qui amènera à l'instruction return. En effet, lors de l'appel à la fonction, on a :
borne_infinitialisé à30;borne_supinitialisé à10;- pour atteindre l'instruction
return, il faudrait que l'utilisateur·trice ait saisi·e un nombrenbà la fois supérieur à30et inférieur à10, ce qui est impossible.
On essayera donc le plus possible de créer des tests dans les fonctions permettant de tester des préconditions, c'est-à-dire des tests vérifiant les propriétés nécessaires concernant les arguments, par exemple ici que les arguments sont bien de type entier, et que borne_inf est bien inférieur ou égal à borne_sup. Ces tests seront vu dans la partie concernant les assertions.
Factoriser le code, 2ème saison
On a vu que factoriser le code, c'est réduire la taille d'un code en évitant les répétitions (principe DRY). Mais en fait, c'est aussi séparer le code en fonctions autonomes respectant une certaine logique interne. Par exemple, dans le code de notre fonction ask_user_int, il y a deux parties bien distinctes :
- la première est de vérifier que la saisie est bien celle d'un entier ;
- la seconde est de vérifier les conditions d'encadrement du nombre saisi.
On pourrait alors souhaiter séparer le code en deux fonctions :
-
créer une fonction
is_integer, dont la signature serait :qui utilise comme paramètredef is_integer(word : str) -> bool :word, une chaine de caractère, et qui renvoie un booléenTruesiwordest transtypable en entier, etFalsesinon. -
puis conserver la fonction
ask_user_inttelle qu'elle est définie actuellement.
On obtiendrait alors le code suivant :
def is_integer(word : str) -> bool :
est_un_entier = True
indice_depart = 0
if word[0] == "-" :
indice_depart = 1
for caractere in word[indice_depart : len(word)] :
if caractere not in "0123456789" :
est_un_entier = False
return est_un_entier
def ask_user_int(borne_min : int, borne_max : int) -> int:
repeter = True
while repeter :
saisie = input(f"Entrez un nombre entre {borne_min} et {borne_max} : ")
if saisie != "" :
if is_integer(saisie) == True:
nb = int(saisie)
if borne_min<= nb <= borne_max :
repeter = False
return nb
Vous noterez que la fonction is_integer est utilisée au sein de la fonction ask_user_int.
Applications
Application 3
Créer une fonction table_multi(m : int)-> str qui prend un argument entier m et écrit la table de multiplication de ce nombre, avec un multiplicateur allant de 1 à 10.
Application 4 : motif dans une chaine
Créer une fonction trouve_chaine(motif : str, texte : str) -> boolqui prend deux arguments, un motif ( une chaine de caractères ) et un texte ( une autre chaine de caractères ) et qui renvoie True si le motif est présent dans la chaine, quel que soit la casse du motif ou celle de la chaine, et False sinon. Vous pouvez tester avec les lignes suivantes :
assert trouve_chaine('Toto', 'Toto va à la plage')==True, 'Meme casse pas trouvée'
assert trouve_chaine('Totos', 'Toto va à la plage')==False, 'Chaine non présente trouvée'
assert trouve_chaine('TOTO', 'Toto va à la plage')==True, 'Problème de majuscules dans le motif'
assert trouve_chaine('toto', 'TOTO va à la plage')==True, 'Problème de minuscules dans le motif'
assert trouve_chaine('ToTo', 'OtOtO va à la plage')==True, 'Que dire ?'
Augmenter la capacité des fonctions : les arguments optionnels
Notre fonction ask_user_int commence à être intéressante. Mais nous pourrions souhaiter personnaliser le message de la question, sans pour autant avoir envie de le changer systématiquement.
C'est tout à fait possible en Python, grâce aux paramètres optionnels. Il s'agit de paramètres dont le nom est donné dans la signature, mais avec une valeur par défaut. Ainsi :
def is_integer(word : str) -> bool :
est_un_entier = True
indice_depart = 0
if word[0] == "-" :
indice_depart = 1
for caractere in word[indice_depart : len(word)] :
if caractere not in "0123456789" :
est_un_entier = False
return est_un_entier
def ask_user_int(borne_min : int, borne_max : int, nom :str = "Inconnu") -> int:
repeter = True
while repeter :
saisie = input(f"{nom}, entrez un nombre entre {borne_min} et {borne_max} : ")
if saisie != "" :
if is_integer(saisie) == True :
nb = int(saisie)
if borne_min<= nb <= borne_max :
repeter = False
return nb
Ainsi, la fonction ci-dessus possède trois paramètres :
- deux paramètres obligatoires,
borne_minetborne_max, de typeint; - un paramètre optionnel,
prenom, de typestr, dont la **valeur par défaut est la chaineInconnu.
Il est à noter qu'impérativement les paramètres obligatoires doivent être placés avant les paramètres optionnels.
On peut alors appeler la fonction des différentes manières suivantes (à tester) :
ask_user_int(0, 10)
ask_user_int(0, 10, nom='Toto')
ask_user_int(25, 40, nom='foo')
Application 5 : paramètres optionnels
Compléter la fonction table_multi afin qu'elle utilise deux paramètres optionnels, la valeur de départ, fixée à 0 initialement, et la valeur d'arrivée du multiplicateur, fixée à 10 initialement.
def table_multi(nb : int, depart : int = 0, fin : int =10) -> str :
table = ''
for i in range(depart, fin+1) :
table += f'{nb}}x{i} = {nb*i} \n'
return table
Application 6 : paramètres optionnels
Réécrire la fonction trouve_chaine afin qu'elle utilise un paramètre booléen optionnel verif_casse, afin de déterminer si le motif est présent dans le texte en vérifiant la casse ou non. Par défaut le paramètre sera False. Vous pouvez utiliser les tests ci-dessous :
##les assertions suivantes sont les même que précédemment
assert trouve_chaine('Toto', 'Toto va à la plage')==True, 'Meme casse pas trouvée'
assert trouve_chaine('Totos', 'Toto va à la plage')==False, 'Chaine non présente trouvée'
assert trouve_chaine('TOTO', 'Toto va à la plage')==True, 'Problème de majuscules dans le motif'
assert trouve_chaine('toto', 'TOTO va à la plage')==True, 'Problème de minuscules dans le motif'
assert trouve_chaine('ToTo', 'OtOtO va à la plage')==True, 'Que dire ?'
# Mais on rajoute celles-ci :
assert trouve_chaine('Toto', 'Toto va à la plage',verif_casse = True )==True, 'Meme casse pas trouvée'
assert trouve_chaine('TOTO', 'TOTO va à la plage',verif_casse = True )==True, 'Meme casse pas trouvée'
assert trouve_chaine('Totos', 'Toto va à la plage',verif_casse = True)==False, 'Chaine non présente trouvée'
assert trouve_chaine('TOTO', 'Toto va à la plage',verif_casse = True)==False, 'Problème de majuscules dans le motif'
assert trouve_chaine('toto', 'TOTO va à la plage',verif_casse = True)==False, 'Problème de minuscules dans le motif'
assert trouve_chaine('ToTo', 'OtOtO va à la plage',verif_casse = True)==False, 'Que dire ?'
def trouve_chaine(motif : str,texte : str, verif_casse : bool =False) -> bool :
if verif_casse==True :
return motif in texte
else :
return motif.lower() in texte.lower() :
Commenter son code : les docstrings
Commenter son code
Une bonne habitude, à prendre immédiatement, est celle de commenter son code, c'est-à-dire d'expliquer l'implémentation de votre code sous la forme de commentaires, écrits en français (ou mieux, en anglais). Les commentaires sont des lignes non-lues par l'interpréteur Python, commençant par le symbole dièse #.
Ces explications sont importantes, car vous vous rendrez vite compte que vous serez parfois incapable de comprendre un code que vous avez écrit vous-même quelques semaines voir quelques jours auparavant !
Alors imaginez celui qui doit lire votre code, mais qui ne l'a pas écrit...
Les concepteur·trice·s de Python ont créé une fonction spécifique permettant d'obtenir des informations sur les autres objets : la fonction help.
Testez par exemple la commande help(print) dans la console, puis essayez avec d'autres objets de Python.
La fonction help va chercher dans l'objet passé en argument sa docstring, littéralement chaine de documentation, qui est une chaine de caractères crée par le ou la codeur·euse présentant l'utilisation de la fonction, ses paramètres obligatoires, ses paramètres optionnels, etc... Une docstring est construite comme une chaine de caractères non nommée présentée immédiatement après la déclaration de la fonction, comme dans l'exemple ci-dessous :
#Import des modules
import doctest
# Déclaration des fonctions
def times3(n) :
"""
Fonction qui multiplie par 3
>>> times3(10)
30
>>> times3(5)
15
>>> times3('a')
'aaa'
"""
return 3*n
# Code réellement exécuté
doctest.testmod() # effectue tous les tests détectés dans les docstrings, et compare au résultat attendu
La fonction somme contient donc une docstring - introduite par trois guillemets (pour permettre les sauts de lignes). Celle-ci décrit l'effet de la fonction, de manière exacte.
On peut alors accéder à la docstring d'une fonction en utilisant la fonction built-in help : testez par exemple help(somme).
Les docstrings
Une docstring est essentielle pour comprendre l'utilité d'une fonction ! Vous devrez en utiliser le plus souvent possible !
Notion de portée des variables
Au sein d'un même programme, les variables définies n'ont pas systématiquement la même portée. La portée d'une variable, c'est l'espace des objets/noms (on parle aussi de monde) dans lequel est défini cette variable.
En utilisant le site Python Tutor, nous allons essayer de comprendre cette notion de portée des variables.
Variables globales et variables locales
Dans l'exemple ci-dessus, la variable mystere est définie dans l'espace de nom global (global frame). Elle est accessible en lecture depuis l'intérieur de la fonction f. On parlera alors de variable globale.
Dans l'exemple ci-dessus, la variable mystere est définie dans l'espace des noms associé à la fonction f, et qui est créé au moment de l'appel à cette fonction (frame f). Cet espace est détruit par le garbage collector dès que l'exécution de la fonction est terminé (une fois la valeur de return renvoyée dans l'espace appelant). Il devient donc impossible d'utiliser la variable mystere puisqu'elle a disparue. mystere est une variable locale à la fonction f.
Impossibilité de modifier une variable globale depuis une fonction appelante
Par construction, il est impossible de modifier directement une variable globale dans une fonction.
Par exemple le code suivant conduira à une erreur UnboundLocalError, car la variable globale mystere n'est pas accessible en modification depuis la fonction f :
Vu que certain.e.s petit.e.s malin.e.s l'ont déjà utilisé en classe, je me dois de préciser une possibilité de modification, mais qui est une très très mauvaise solution, reconnue unanimement comme étant à éviter le plus possible : il s'agit d'utiliser le mot clé global dans la fonction. Par exemple avec le code suivant :
Utiliser les paramètres d'une fonction
Dans l'exemple ci dessus :
- une variable globale
xest crée avec une valeur entière 5 ; - cette variable est passée en paramètre à la fonction, ce qui crée une variable locale
x, valant 5 ; - la valeur de la variable locale
xest doublée. Elle vaut alors 10 ; - une fois la fopnction terminée, l'espace de nom de la fonction
fest détruit, et donc la variable localexest aussi détruite ; - par contre la variable globale
xn'a pas vu sa valeur modifiée. Elle reste égale à 5.
Utiliser la valeur de retour d'une fonction
Ici, on a changé les lignes 4 et 8 :
- dans la fonction, la ligne 4
return xindique que la valeur de la variable localexdoit être renvoyée dans l'espace global ; - dans le code général, la ligne 8 indique que la variable
xest réaffectée avec la valeur de retour de la fonctionf.
Tests et assertions
Préconditions
Comme nous l'avons vu pour la fonction ask_user_int, il est souvent nécessaire de tester des préconditions sur les arguments d'une fonction, pour s'assurer que celle-ci fonctionnera bien selon le schéma voulu.
Une méthode pour tester les préconditions est d'utiliser des assertions.
L'instruction assert de Python fonctionne de la manière suivante :
assert trouve_chaine('Toto', 'Toto va à la plage')==True, 'Meme casse pas trouvée'
L'instruction assert teste un booléen, ici trouve_chaine('Toto', 'Toto va à la plage')==True. Il peut alors se produire deux cas :
- soit le booléen est
True, auquel cas l'interpréteur passe à la ligne suivante (il ne se passe rien de visible) ; - soit le booléen est
False, auquel cas l'interpréteur arrête le code en levant une erreur de typeAssertionErroret affiche la chaine de caractère passée en second argument, ici'Meme casse pas trouvée'.
Correction de la fonction ask_user_int
def ask_user_int(borne_min : int, borne_max : int, nom : str ='Inconnu' ) -> int :
""" Fonction demandant un entier compris entre borne_nf et borne_sup.
personnalisation du message avec le paramètre optionnel nom.
renvoi un objet de type entier.
"""
assert type(borne_inf) == int and type(borne_sup) == int, "Erreur de type sur borne_inf et borne_sup"
assert type(nom) == str, "Erreur de type sur nom"
assert borne_inf <= borne_sup , "borne_inf n'est pas inférieure à borne_sup"
repeter = True
while repeter :
...# Le code ici n'est pas changé
Testez maintenant cette fonction avec de mauvaises valeurs pour les arguments.
Réfléchir avant d'agir : écrire les tests avant la fonction
Lorsqu'on écrit une fonction, il est très important d'avoir une idée précise de ce que la fonction doit renvoyer, y compris dans les cas extrêmes ou cas spécifiques.
Par exemple, on pourrait considérer une fonction coefficient_directeur qui donne le coefficient directeur d'une droite quand on lui passe en argument les coordonnées de deux points \(A\) et \(B\).
Je rappelle que le calcul du coefficient directeur de la droite \((AB)\) se fait par l'intermédiaire de la formule :
Ainsi nous aimerions que la fonction travaille avec 4 paramètres de type entiers xA, yA, xB, yB, qu'elle renvoie le coefficient directeur de \((AB)\) sous la forme d'un flottant si ce coefficient existe, et qu'elle renvoie None quand il n'existe pas. On, définit ici ce qu'on appelle une interface de la fonction :
def coefficient_directeur(xA : int, yA : int, xB : int, yB : int) -> float :
""" Renvoie le coefficient directeur de la droite (AB) telle que :
A (xA ; yA) et B(xB ; yB), et les coordonnées de A ety B sont entières.
Dans le cas ou le coefficient directeur n'existe pas, renvoie None
"""
...
On souhaite donc prévoir en avance différents cas de fonctionnement de la fonction, et on aimerait vérifier que les calculs effectués par la fonction correspondent bien à ces cas.
Par exemple, nous aimerions que la fonction vérifie le test suivant :
>>> coefficient_directeur(2, 4, 3, 7)
3.0
Écrire des tests
- Écrire 5 tests prenant en compte tous les cas possibles d'utilisation de la fonction, en supposant que les types fournis en argument soient bien des entiers.
- Compléter la fonction
coefficient_directeurafin qu'elle remplisse le rôle qui lui est demandé.
A venir !
Tester avec le module doctest (Non traité en classe)
Pyhton étant user friendly, il permet au programmeur de tester automatiquement, grâce au module doctest.
Un module Python est un fichier (ou un ensemble de fichiers) qui comporte(nt) des objets et des fonctions qui peuvent être ajoutés aux fonctionnalité de base de Python. Il en existe un très grand nombre, tous étant spécialisés dans un domaine. On trouve par exemple :
- le module math, qui contient beaucoup de fonctions mathématiques ;
- le module turtle, qui permet de dessiner géométriquement ;
- le module pygame, qui est un module permettant de gérer les différents éléments d'un jeu vidéo ;
- le module panda, qui est utilisé pour faire du traitement de données ;
- le module flask, qui permet de créer une application web
- ...
Un module doit être chargé en mémoire une fois (de préférence au début du code), par l'intermédiaire de la commande :
import nom_du_module
Mais non, ca ne se fait pas que comme ça !
Oui, vous avez raison, mais ce n'est pas le cas de ce cours, nous verrons l'import de modules plus tard dans l'année...
Le module doctest, lui, permet d'intégrer à la docstring un ensemble de tests qui sont vérifiés par l'appel de la fonction testmod du module doctest.
Par exemple, copiez-collez le code suivant dans un fichier :
# Zone d'import des modules
import doctest # On charge en mémoire le module doctest
# Zone de déclaration des fonctions
def times2(n : object) -> object:
"""
Fonction qui renvoie le double, selon le type d'objet
>>> times2(4)
8
>>> times2(5.4)
10.8
>>> times2('a')
'aa'
"""
return 2*n
# Zone du script global
doctest.testmod() # Lance les tests des fonctions déclarées
L'appel à la fonction doctest.testmod() déclenche les trois tests présents dans la doctsring, et vérifie que les résultats de la fonctions correspondent à ceux fournit dans la docstring.
Application 7 : Ajouter une fonction et des tests
Créez une fonction times3 qui renvoie le triple de l'objet passé en argument, l'objet pouvant êtrte de type int, float ou str, en écrivant les tests correspondants.
A venir !
Je me servirai de doctests ou bien d'assertions pour estimer la justesse de vos fonctions et de vos programmes. Une bonne idée serait de TOUJOURS concevoir un jeu d'exemple avant de se lancer dans la construction d'une fonction.
Exercices
Dans tous les exercices suivants, on devra écrire pour chacune des fonction des assertions permettant de tester les préconditions.
Vous devrez tester les fonctions avec les jeux de tests fournis dans les docstring.
Vous pourrez créer un seul fichier contenant l'ensemble des fonctions ci-dessous.
-
Ecrire une fonction qui renvoie le maximum de deux nombres
ìntdonnés :def maxi(a : int,b :int) -> int : """ Fonction qui renvoie le maximum de deux nombres de type int. >>> maxi(12,3) 12 >>> maxi(-5,9) 9 >>> maxi(6,6) 6 """ ... -
Écrire une fonction qui renvoie le minimum de deux nombres
intdonnés :def mini(a : int, b : int ) -> int : """ Fonction qui renvoie le minimum de deux nombres >>> mini(12,3) 3 >>> mini(-5,9) -5 >>> mini(6,6) 6 """ ... -
Écrire une fonction qui renvoie le maximum de trois nombres
intdonnés :def maxi3(a : int, b : int, c : int) -> int : """ Fonction qui renvoie le maximum de trois nombres >>> maxi3(5,12,3) 12 >>> maxi3(-5,-7,2) 2 >>> maxi3(5,1,2) 5 >>> maxi3(6,6,6) 6 >>> maxi3(5,7,7) 7 >>> maxi3(-2, -2, -3) -2 """ ... -
Écrire une fonction qui supprime tous les caractères qui ne sont pas des lettres (majuscules ou minuscules, sans accents) d'une chaine de caractères donnée.
def rien_que_des_lettres(chaine :str) -> str : """ >>> rien_que_des_lettres(' toto ') 'toto' >>> rien_que_des_lettres('123Toto456') 'Toto' >>> rien_que_des_lettres("Et!C'est Toto ?") 'EtCestToto' """ ...