fonctions python

Identification

Infoforall

12 - Fonctions et variables


Vous savez déclarer des fonctions, et faire appel à ces fonctions. Nous allons voir aujourd'hui les étudier un peu plus en détails.

La doc
Ne pas lire la documentation...

Prérequis : Algo 3-4-5 pour avoir des notions sur l'implication et connaître les algorithmes de recherches linéaires.

Evaluation: 7 questions

  07-11-12-13-14-19

  20

Exercices supplémentaires 🏠 : oui

Documents de cours PDF : .PDF

Sources latex : .TEX et entete.tex et licence.tex

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

1 - Les bases

Commençons par revoir ce que vous savez déjà sur les fonctions.

Ne zappez pas les rappels si vous ne les avez jamais vu. Sinon, le reste de l'activité risque de vous sembler difficile...

Rappels

(Rappel) 1.1 FONCTION : notion
ENTREE(S)  ⇒   Fonction   ⇒  SORTIE

Principe général

Une fonction est un bloc de code qui reçoit des données d'entrée et envoie une sortie.

Un exemple avec la fonction VALEUR_ABSOLUE qui n'attend qu'une entrée (un nombre) et qui renvoie la valeur absolue du nombre (le même nombre mais sans son signe).

-12  ⇒   Fonction VALEUR ABSOLUE   ⇒  12

12  ⇒   Fonction VALEUR ABSOLUE   ⇒  12
D'autres exemples
5, 10, 50, 0, 20  ⇒   Fonction MAXIMUM   ⇒  50

5, 10, 50, 0, 20  ⇒   Fonction MINIMUM   ⇒  0

5, 10, 50, 0, 20  ⇒   Fonction SOMME   ⇒  85
(Rappel) 1.2 - FONCTION : native

Une fonction native est une fonction présente de base dans Python.

Dans le cadre de ce site, les fonctions natives sont toujours écrites en vert.

On fait appel à une fonction en notant son nom suivi de parenthèses contenant les entrées séparées par des virgules :

nom_fonction(a, b, c, d...)

Fonction VALEUR ABSOLUE : cette fonction se nomme abs() en Python.

>>> abs(-12) 12 >>> abs(12) 12

Fonction MAXIMUM : cette fonction se nomme max() en Python.

>>> max(10, 0, 50, 40) 50
(Rappel) 1.3 FONCTION : définition d'une fonction personnelle
1 2 3 4
def double(x): # Déclaration return 2 * x # Déclaration n = double(12) # Appel

Définir une fonction revient à stocker ses instructions pour les utiliser plus tard.

On voit d'ailleurs que vous ne pourriez pas l'utiliser pour le moment car vous ne savez pas ce que contient réellement la variable x.

Par contre, si vous avez bien compris ce qu'est une variable, vous devriez comprendre qu'on va remplir le double de ce qu'on va recevoir un jour en entrée.

Pour définir une fonction, on utilise le mot-clé def puis

  1. le nom de la fonction
  2. des parenthèses contenant les entrées séparées par des virgules
  3. le caractère : pour finir cette première ligne
  4. le bloc tabulé contenant les instructions à réaliser un jour.
1 2
def double(x): return 2 * x

Le mot-clé return permet d'indiquer la sortie que la fonction devra renvoyer.

Placer des commentaires en français dans le code Python

On peut placer du texte à destination des développeurs qui vont lire le programme en utilisant le caractère #. Voir la partie suivante pour un exemple.

Attention : sur votre ligne, tout ce qui se trouve derrière ce # sera purement ignoré par l'interpréteur.

(Rappel) 1.4 FONCTION : appel de fonction
1 2 3 4
def double(x): # Déclaration return 2 * x # Déclaration n = double(12) # Appel

Lancer un appel de fonction
Lancer un appel veut dire utiliser une fonction en lui fournissant des entrées puis attendre pour récupérer la réponse qu'elle fournira.

Sur le code d'exemple, on lance un appel en ligne 4.

Lors d'un appel, l'interpréteur Python transfère les données envoyées une à une dans les variables visibles sur la définition. Pour comprendre où sont stockées les entrées, il suffit de comparer la ligne de la définition et celle de l'appel.

Avec l'appel de la ligne 5, on a 10 qui est stocké dans a lors de cet appel :

définition appel
def fois2(a): x = fois2(10)
[Déroulé] Lors d'un appel, on part de la ligne d'appel vers la ligne de définition puis la réponse est renvoyée vers la ligne d'appel.

1 2 3 4
def double(x): # Définition return 2 * x # Définition n = double(12) # Appel
  • L1 (déclaration)
  • L4 (appel) - L1 - L2 (envoi de la réponse)
  • L4 (réception de la réponse) - fin car il n'y a rien ensuite

Explications complètes

  • Ligne 1 : déclaration d'une fonction double() qui va recevoir une donnée qu'on placera dans une variable x.
  • Ligne 4 : appel à la fonction double() en lui envoyant 12
  • Ligne 1 : l'interpréteur place 12 dans x.
  • Ligne 2 : la fonction envoie la réponse 24 à la ligne qui a lancé l'appel
  • Ligne 4 : réception de la réponse et n référence 24.
Lancer plusieurs appels

Les fonctions ont deux avantages :

  1. Permettre l'envoi d'une sortie dont la valeur dépend des entrées reçues ;
  2. Permettre plusieurs appels successifs à une même fonction.
1 2 3 4 5
def double(x): return 2 * x a = double(10) b = double(30)

Déroulé

  • L1 (déclaration)
  • L4 (appel) - L1 - L2 (envoi de la réponse)
  • L4 (réception de la réponse et affectation du 20 à a)
  • L5 (appel) - L1 - L2 (envoi de la réponse)
  • L5 (réception de la réponse et affectation du 60 à b) - fin
Bilan

Vous devez donc savoir distinguer ces deux étapes, sous peine de ne pas comprendre correctement ce qui est réalisé :

  1. La définition (mise en mémoire d'un bloc d'instructions pour l'utiliser plus tard)
  2. L'appel (utilisation de ce bloc d'instructions)
Connaissance vitale : "Définir" n'est pas "Faire appel"

Il faut vraiment comprendre qu'après avoir lu la définition de votre fonction, l'interpréteur Python ne lance pas d'appel à votre fonction. D'ailleurs, il aurait du mal... Que mettrait-il dans x de toutes manières ?

Définir/déclarer une fonction permet juste de mettre ce nom en mémoire pour pouvoir y faire appel plus loin dans le programme.

(Rappel) 1.5 FONCTION : plus complexe
1 2 3 4 5 6 7
def nouvelle_note(note): note = note + 5 if note > 20: note = 20 return note n = nouvelle_note(12)

Cette fonction récupère la valeur d'une note, rajoute 5, bloque la note à 20 si elle dépassait 20 et renvoie le résultat final.

Déroulé

  • L1 (déclaration)
  • L7 (appel) - L1 - L2 - L3 - L5 (envoi) - L7 (réception) - fin

Traduction et déroulé

  • Ligne 1 : déclaration de nouvelle_note() qui va recevoir une donnée qu'on placera dans note.
  • Ligne 7 (appel à la fonction nouvelle_note() en lui envoyant 12).
  • Ligne 1 à 5 : on exécute les lignes avec note contenant 12 initialement. Après la ligne 2, note contient donc 17. Le test de la condition étant alors faux (17 n'est pas supérieur à 20), on passe en ligne 5 : la fonction envoie 17 en sortie.
  • L7 (réception) : on stocke 17 dans n.

⚙ révisions-01° Donner la séquence de lignes suivies par Python puis la réponse finale qu'on stocke dans x.

1 2 3 4 5 6
def plus10(a): return a + 10 x = plus10(5) print(x) print("FIN")

...CORRECTION...

L01(définition)

L04(appel en envoyant 5) - L01(réception du 5 dans a) - L02(envoi de 15)

L04(retour du 15 qu'on stocke dans x) - L05 - L06

⚙ révisions-02° Observer ce nouveau programme puis répondre aux questions.

1 2 3 4 5
def mystere(a, b, c): reponse = a + b*2 + c return reponse x = mystere(5, 10, 30)

Questions

  1. Sur quelle ligne se fait l'appel ?
  2. Que vaut a lors de cet appel ?
  3. Que vaut b ?
  4. Que vaut c ?
  5. Que va alors contenir x une fois que la fonction aura répondu ?

...CORRECTION...

  1. Sur quelle ligne se fait l'appel ?
  2. Ligne 5

  3. Que vaut a lors de cet appel ?
  4. Que vaut b ?
  5. Que vaut c ?
  6. Il faut comparer la définition et l'appel :

    définition appel
    def mystere(a, b, c): x = mystere(5, 10, 30)

    a reçoit 5, b reçoit 10 et c reçoit 30.

  7. Que va alors contenir x une fois que la fonction aura répondu ?
  8. Lors de cet appel, la fonction réalise le calcul 5 + 10*2 + 30 ce qui donne 55.

    La réponse va renvoyer ce 55, qui sera alors stocké dans la variable x.

⚙ révisions-03° Fournir la séquence de lignes suivie par Python.

1 2 3 4 5 6 7 8
def mystere(a, b, c): reponse = a + b*2 + c return reponse print("Avant l'appel") x = mystere(5, 10, 30) print("Après l'appel") print(x)

...CORRECTION...

L01(définition)

L05

L06(appel en envoyant 5-10-30) - L01(réception du 5 dans a ect...) - L02(calcul de 80) - L03(envoi de 80)

L06(retour du 80 qu'on stocke dans x) - L07 - L08

⚙ révisions-04° Utiliser ce dernier programme : on définie une fonction carre() qui permet de dessiner un carré de 20 pixels de large en fournissant les coordonnées x et y voulues ainsi que la couleur.

  • Lancer le programme puis
  • réaliser un petit dessin en pixel art contenant 9 appels (et donc 9 carrés).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
from turtle import * def carre(x, y, c): largeur = 20 penup() goto(x*largeur, y*largeur) pendown() setheading(0) # on force la tortue à regarder à droite pencolor(c) # on définit la couleur du crayon fillcolor(c) # on définit la couleur de remplissage begin_fill() # demande de surveillance de ce qui est dessiné for _ in range(4): forward(20) left(90) end_fill() # fin du dessin, turtle peut maintenant colorier l'intérieur carre(0, 0, "#FF0000") # carré rouge carre(1, 0, "#00FF00") # carré vert carre(0, 1, "#0000FF") # carré bleu carre(0, 2, "#FFFF00") # carré jaune

⚙ révisions-05° Compléter la fonction addition() : elle doit renvoyer l’addition de deux entiers qu’on fournira lors de l’appel. Compléter également le deuxième appel pour lui envoyer les valeurs 10 et 15 par exemple.

01 02 03 04 05 06 07 08
def addition(a, b): ... x = addition(10, 40) print(x) y = addition(..., ...) print(y)

...CORRECTION...

01 02 03 04 05 06 07 08
def addition(a, b): return a + b x = addition(10, 40) print(x) y = addition(10, 15) print(y)

⚙ révisions-06° Compléter la fonction affine() pour renvoyer a*x + b à partir des paramètres a, b et x.

01 02 03 04 05 06 07 08
def affine(a, x, b): ... r1 = affine(2, 4, 3) print(r1) r2 = affine(..., ..., ...) print(r2)

Sur votre feuille : noter la définition de la fonction sur votre copie puis l’appel qu’on doit en faire pour connaître l'image de la fonction avec a=10, x=2 et b=100.

...CORRECTION...

01 02 03 04 05 06 07 08
def affine(a, x, b): return a*x + b r1 = affine(2, 4, 3) print(r1) r2 = affine(10, 2, 100) print(r2)

⚙ révisions-07° Créer une fonction carre() qui a x associe x2.

...CORRECTION...

01 02 03 04 05
def carre(x): return x*x r1 = carre(10) print(r1)

✔ 08° Tapez ceci dans la console pour vous souvenir de la façon de tester une égalité entre deux variables a et b.

>>> a = 10 >>> b = 20 >>> a == b False >>> a*2 == b True >>> a == b False

⚙ révisions-09° Compléter la dernière ligne de verification() pour renvoyer True si le paramètre mot_recu passé en paramètre est le même que le bon mot de passe, contenu dans une variable mdp, locale à la fonction. Sinon, elle renvoie False.

01 02 03 04 05
def verification(mot_recu): mdp = '1234!' return ... print(verification(...))

...CORRECTION...

01 02 03 04 05
def verification(mot_recu): mdp = '1234!' return mdp == mot_recu print(verification("0000"))

⚙ révisions-10° Compléter ce programme pour qu'il demande le mot de passe jusqu'à ce que l'utilisateur donne le bon.

01 02 03 04 05 06 07 08 09 10
def verification(mot_recu): mdp = '1234!' return ... tentative = "" while verification(tentative) == ...: print("Donnez le mot de passe :") tentative = input() print("Autorisation accordée.")

...CORRECTION...

01 02 03 04 05 06 07 08 09 10
def verification(mot_recu): mdp = '1234!' return mdp == mot_recu tentative = "" while verification(tentative) == False: print("Donnez le mot de passe :") tentative = input() print("Autorisation accordée.")

⚙ révisions-11° Utiliser la fonction ci-dessous pour vérifier qu’elle renvoie la même chose que la précédente (mais en plus "naïve" puisque cette version teste si c'est Vrai pour renvoyer Vrai, sinon elle renvoie Faux).

Question

Un utilisateur peut-il se rendre compte que les instructions internes des deux fonctions sont différentes ?

1 2 3 4 5 6
def verification_v2(mot_recu): mdp = '1234!' if mot_recu != mdp: return False else: return True

RAPPEL sur != : cet opérateur booléen teste la différence entre les deux termes à sa gauche et à sa droite.

...CORRECTION...

L'utilisateur ne pourra pas voir la différence.

En réalité, il existe une infinité de façon de réaliser une fonction répondant d'une certaine façon.

Il existe même un théorème qui permet d'affirmer qu'on ne peut pas créer d'algorithme capable de dire si une fonction donnée est nécessairement la plus courte pour résoudre le problème qu'on lui pose.

⚙ révisions-12° Compléter la fonction verification_v3() pour qu'elle utilise plutôt l'opérateur d'égalité (==). Son utilisation doit renvoyer exactement les mêmes réponses : seule la façon dont elle est conçue à l'interne diffère. Du point de vue de l'extérieur, on ne doit se rendre compte de rien.

1 2 3 4 5 6
def verification_v3(mot_recu): mdp = '1234!' if ... == ...: return ... else: return ...

...CORRECTION...

1 2 3 4 5 6
def verification_v3(mot_recu): mdp = '1234!' if mdp == mot_recu: return True else: return False

⚙ révisions-13° Créer une fonction plus_grand(a, b) qui doit renvoyer la plus grande valeur fournie entre a et b.

1 2 3 4 5
def plus_grand(a, b): if ... >= ...: return ... else: return ...

...CORRECTION...

1 2 3 4 5
def plus_grand(a, b): if a >= b: return a else: return b

⚙ révisions-14° Expliquer le contenu des variables à la fin du programme.

1 2 3 4 5 6 7 8
def facile(x): return x*10 + 5 def moins_facile(x, y, z): return x*100 + y*10 + z resultat1 = facile(3) resultat2 = moins_facile(5, 6, 3)

Puisque le programme ne possède aucun appel à print(), il ne provoque aucun affichage.

Par contre, vous pouvez lancer le programme puis interroger Python via la console interactive. C'est l'une des puissances de Python : on peut mixer programme et interaction directe.

>>> resultat1 ??? >>> resultat2 ???

...CORRECTION...

Le 3 est stocké dans la variable locale x. La première fonction calcule donc 3*10+5, et renvoie 35.

En comparant appel et déclaration pour la deuxième fonction, on voit que x reçoit 5, y reçoit 6 et z reçoit 3. La fonction calcule 500+60+3 et renvoie 563.

2 - Vocabulaire : arguments et paramètres

2.1 return : on peut fournir une expression

Lorsque l'interpréteur rencontre le mot-clé return, il réalise trois choses :

  1. Il évalue l'expression derrière le return ;
  2. Il quitte définitivement l'appel ;
  3. Il renvoie la sortie à l'endroit d'où a été lancé l'appel.

De plus, si une fonction ne rencontre pas de return avant la fin de son exécution, alors la fonction renvoie automatiquement une valeur particulière : None, pour signifier "rien".

En conséquence, deux choix d'écriture sont possibles :

    1 2 3
    def fois2(x): resultat = x * 2 return resultat
    1 2
    def fois2(x): return x * 2
return n'est pas une fonction

Ne placez pas de parenthèses derrière le return. Sinon, vous laissez croire à un débutant qu'il s'agit d'une fonction.

Seule exception : lorsque les parenthèses sont nécessaires à cause des priorités.

Rajoutons deux nouvelles définitions à connaître à partir de cette activité.

2.2 FONCTION : paramètre et argument
1 2 3 4 5
def valeur(d, u): # d et u sont les paramètres déclarés resultat = d*10 + u return resultat v = valeur(5, 3) <-- 5 et 3 sont les arguments de l'appel

Argument et paramètre
Un argument est une valeur d'entrée qu'on fournit à la fonction lors d'un appel.

Sur l'exemple, 5 et 3 sont des arguments.

Un paramètre est une variable locale présente sur la première ligne de la définition. Lors de l'appel, les paramètres servent à stocker les arguments reçus.

Sur l'exemple, d et u sont des paramètres (des variables locales).

Un paramètre n'a d'existence que pendant l'exécution de l'appel  : c'est une variable locale.

Cas d'une fonction avec 1 paramètre
1 2 3 4
def fois2(x): ... v = fois2(20)

Lors de l'appel L4, on envoie l'argument 20 qu'on stocke dans le paramètre x

Cas d'une fonction avec 2 paramètres
1 2 3 4
def valeur(d, u): ... v = valeur(5, 3)

Lors de l'appel L4, on envoie les arguments 5 et 3 qu'on stocke dans les paramètres d et u

Moyen mnémotechnique pour les interros

appel (commence par a) : argument (commence par a)

définition/déclaration (commence par d) : paramètre (commence par p, un d à l'envers)

Vocabulaire alternatif

On remplace parfois le terme paramètre par le terme paramètre formel.

On remplace parfois le terme argument par le terme paramètre effectif.

✔ 01-A° Réaliser les 3 actions suivantes 

  1. Ouvrir l'onglet VARIABLES de Thonny pour vérifier que la mémoire est vide initialement
  2. Sauvegarder et lancer le programme suivant sous le nom activite_fonctions.py.
  3. 1 2 3 4 5 6 7
    def fois2(x): resultat = x * 2 return resultat def plus2(x): resultat = x + 2 return resultat
  4. Vérifier via l'onglet VARIABLES que la mémoire contient maintenant deux nouvelles "variables" nommées fois2 et plus2 de type function.
  5. résultat dans thonny

✔ 01-B° Réaliser cette dernière action 

Tapez fois2 (le nom, sans parenthèse) : cela ne lance aucun appel. Python vous indique que cette "variable" est de type natif function : elle référence un bloc d'instructions.

>>> fois2 <function fois2 at 0x7f2d96628af0> >>> type(fois2) <class 'function'>

Le at 0x fait référence à l'adresse mémoire où la fonction est stockée. Le x indique que l'adresse est fournie en hexadécimal. L'adresse sera différente sur votre machine bien entendu.

⚙ 02° Réaliser ceci :

A - AVANT d'avoir exécuté quoi que ce soit

  • Les valeurs 2, 3, 5, 7, 9, 0 sont-elles des paramètres ou des arguments ?
  • Les variables u et d sont-elles des paramètres ou des arguments ?
  • Réaliser mentalement la succession des lignes suivies par l'interpréteur Python lors de l'exécution de ce programme.
1 2 3 4 5 6 7
def valeur(d, u): resultat = d*10 + u return resultat a = valeur(2, 3) b = valeur(5, 7) c = valeur(9, 0)

B - Python Tutor

Aller sur Python Tutor, et visualiser le déroulé du programme en mode pas à pas, cela vous permettra de vérifier si votre parcours était le bon.

...CORRECTION...

Partie A

Les entiers sont des arguments puisqu'on les envoie pendant un appel à la fonction pour qu'elle puisse travailler.

Les variables u et d sont des paramètres, des variables locales permettant de stocker temporairement les arguments reçus.

L1 (déclaration)

L5 (appel en envoyant 2 et 3) - L1 - L2 - L3(réponse)

L5 (réception puis affectation)

L6 (appel en envoyant 2 et 3) - L1 - L2 - L3(réponse)

L6 (réception puis affectation)

L7 (appel en envoyant 2 et 3) - L1 - L2 - L3(réponse)

L7 (réception puis affectation)

Fin

✔ 03° Validons cette première partie : appuyez sur le bouton VISUALISER ci-dessous, vous visualiserez l'ordre d'exécution de ce programme

Normalement, cela devrait vous sembler évident, sinon, pensez à me demander des explications !

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
# 3 - - Déclaration des fonctions - - - - def fois2(x): resultat = x * 2 return resultat def plus2(x): resultat = x + 2 return resultat # 4 - - Programme principal - - - - - nombre = 3 nombre2 = 5 a = plus2(nombre) b = fois2(a) b = b + 1

CLIQUEZ ICI POUR VOIR L'ORDRE DES INSTRUCTIONS EXECUTEES :

3 - Documentation : expliquer comment utiliser

Le but de l'informatique ?

Résoudre des problèmes en utilisant des programmes basés sur des algorithmes.

Qu'est-ce qu'un beau programme ?

  • Point 1 : d'abord des fonctions qui fonctionnent en donnant toujours le bon résultat
  • ALGORITHMIQUE : preuve de correction.

  • Point 2 : ensuite des fonctions suffisamment compréhensibles :
  • Compréhensible par les concepteurs : COMMENTAIRE à destination des autres développeurs qui auraient à modifier le code interne de la fonction.

    Compréhensible par les utilisateurs : DOCUMENTATION à destination des utilisateurs qui auront à lancer des appels.

  • Point 3 : enfin, des fonctions qui répondent rapidement
  • ALGORITHMIQUE : notion de coûts.

(Rappel) 3.1 COMMENTAIRES : expliquer le fonctionnement

Les commentaires
Ne pas écrire de commentaires...

Les commentaires sont destinés à un développeur humain et visent à fournir des explications sur le fonctionnement des parties les plus difficiles du code interne : un bon commentaire est un commentaire utile, il doit permettre de comprendre et modifier un code même plusieurs années après sa création initiale.

Pour placer un commentaire en Python, on utilise le caractère dièse (#). Trois exemples à comprendre :

  • Commentaire sur toute une ligne (ligne 1 ci-dessous)
  • 1
    # Toute cette ligne est un commentaire.
  • Commentaire en fin de ligne (ligne 2 ci-dessous)
  • 2
    print("Bonjour tout le monde") # Ceci est également un commentaire
  • La ligne 3 ne comporte aucun commentaire puisque le # fait juste partie d'un string.
  • 3
    print("Cette ligne ne contient pas de # commentaire")

Il reste à voir la documentation : comment parvenir à créer des fonctions facilement UTILISABLES même si nous n'en sommes pas le créateur.

3.2 DOCUMENTATION : expliquer l'utilisation

Les commentaires
Ne pas lire la documentation...
A - Documenter pour savoir utiliser

La documentation explique à l'utilisateur comment lancer un appel à la fonction, sans provoquer d'erreur.

La documentation minimale contient les informations nécessaires pour utiliser la fonction sans provoquer d'erreurs et comporte donc :
  1. Une phrase décrivant le but de cette fonction ;
  2. le type et le nombre d'arguments à lui envoyer ;
  3. le type et le contenu de la réponse.
B - Obtenir la documentation

Soit on la cherche directement dans le code, soit on utilise ceci :

La fonction native help() permet de récupérer la documentation d'une fonction.

Pour récupérer la documentation d'une fonction, on lui envoie en argument le nom de la fonction voulue sans les parenthèses. Ainsi, help(len) renvoie la documentation de la fonction native len().

Pour len() :

>>> help(len) Help on built-in function len in module builtins: len(obj, /) Return the number of items in a container.

Pour la fonction randint() du module random :

>>> import random >>> help(random.randint) Help on method randint in module random: randint(a, b) method of random.Random instance. Return random integer in range [a, b], including both end points.

Pour la fonction rect() du module pyxel :

>>> import pyxel >>> help(pyxel.rect) Help on built-in function rect in module pyxel.pyxel_wrapper: rect(x, y, w, h, col)

Comme vous le voyez, la documentation est parfois minimaliste, surtout sur les projets personnels, comme Pyxel.

⚙ 06-A° Installer le module pyxel sur votre poste. Au lycée, il faut faire passer Thonny sur la dernière version de Python qui se trouve sur votre poste (Thonny -> outils -> interpréteur) puis chercher l'exécutable python dans les programmes.

Ensuite ouvrir la console système de Thonny et taper ceci :

pip install -U pyxel

Le paramètre -U sert à forcer la mise à jour vers la dernière version de pyxel.

Une fois l'installation terminée, il faut vérifier que tout est bien installé à l'aide de cette instruction dans la console Python de Thonny :

>>> import pyxel

Nous allons maintenant utiliser le module pyxel.

Son principe est simple : 30 fois par seconde, il réalise

  • d'abord un appel à controler()
  • puis un appel à afficher().

⚙ 06-B° Voici ce qu'affiche la documentation de la fonction rect() du module pyxel. Cette fonction permet de tracer des rectangles. x et y sont les coordonnées en pyxel, col la couleur (un entier compris entre 0 et 15), h pour height/hauteur et w pour width/largeur.

>>> import pyxel >>> help(pyxel.rect) Help on built-in function rect in module pyxel.pyxel_wrapper: rect(x, y, w, h, col)

Question : compléter le code interne de afficher() pour que cette fonction parvienne à réaliser ceci :

  • mémoriser dans xp la valeur associée à la clé 'x' de la variable personnage ;
  • mémoriser dans yp la valeur associée à la clé 'y' de la variable personnage
  • dessiner un rectangle de 4 pixels sur 8 placé en (xp;yp) et de couleur 1 ;
  • dessiner un rectangle de 40 pixels sur 4 placé en (30;40) et de couleur 10 ;
  • dessiner un rectangle de 60 pixels sur 20 placé en (50;75) et de couleur 9 ;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
import pyxel import random personnage = {'x':60, 'y':15} def controler(): """Modifie les données 30 fois par seconde""" pass def afficher(): """Modifie l'affichage 30 fois par seconde""" pyxel.cls(0) xp = ... yp = ... ... ... ... pyxel.init(128, 128, title="Mon premier jeu") # Initialisation de la fenêtre pyxel.run(controler, afficher) # Alternance controler/afficher

...CORRECTION...

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
import pyxel import random personnage = {'x':60, 'y':15} def controler(): """Modifie les données 30 fois par seconde""" pass def afficher(): """Modifie l'affichage 30 fois par seconde""" pyxel.cls(0) xp = personnage['x'] yp = personnage['y'] pyxel.rect(xp, yp, 4, 8, 1) pyxel.rect(30, 34, 40, 4, 10) pyxel.rect(50, 75, 60, 20, 9) pyxel.init(128, 128, title="Mon premier jeu") # Initialisation de la fenêtre pyxel.run(controler, afficher) # Alternance controler/afficher

⚙ 06-C° Pour gérer la gravité, nous allons utiliser la technique suivante : si le pixel sous le personnage est de couleur noire (valeur 0) alors on s'arrange pour que le personnage descende de 1 pixel. Attention : l'axe Oy est orienté vers le bas, descendre veut donc dire que la coordonnée y du personnage augmente de 1.

Les parties modifiées par rapport à la question précédente sous en jaune pale.

La fonction pget() du module pyxel permet de récupérer la couleur du pyxel dont on donne les coordonnées. On rappelle que dans le module Pyxel, les couleurs sont des entiers. pget() signifie "pixel get".

  1. Lancer le code pour voir le résultat.
  2. Expliquer mentalement pourquoi le personnage tombe jusqu'à percuter une plateforme.
  3. Rajouter des commentaires utiles derrière les lignes de code de la fonction controler() qui méritent une explication.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
import pyxel import random personnage = {'x':90, 'y':0} def controler(): """Modifie les données 30 fois par seconde""" xp = personnage['x'] yp = personnage['y'] ligne_en_dessous = yp + 8 # On rajoute 8 car le personne a une hauteur de 8 if pyxel.pget(xp, ligne_en_dessous) == 0: personnage['y'] = personnage['y'] + 1 def afficher(): """Modifie l'affichage 30 fois par seconde""" pyxel.cls(0) xp = personnage['x'] yp = personnage['y'] pyxel.rect(xp, yp, 4, 8, 1) pyxel.rect(30, 34, 40, 4, 10) pyxel.rect(50, 75, 60, 20, 9) pyxel.init(128, 128, title="Mon premier jeu") # Initialisation de la fenêtre pyxel.run(controler, afficher) # Alternance controler/afficher

...CORRECTION...

30 fois par seconde, l'ordonnée du personnage (stockée dans le dictionnaire personnage) augmente de 1 si on détecte que le pixel sous le personnage est noir. L'axe y étant orientée vers le bas, le personnage descend à l'écran.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
import pyxel import random personnage = {'x':90, 'y':0} def controler(): """Modifie les données 30 fois par seconde""" xp = personnage['x'] yp = personnage['y'] ligne_en_dessous = yp + 8 # On rajoute 8 car le personne a une hauteur de 8 if pyxel.pget(xp, ligne_en_dessous) == 0: # s'il n'y a rien sous le personnage, ce pyxel est noir (0) personnage['y'] = personnage['y'] + 1 # le personnage tombe de 1 pixel puisque y est orientée vers le bas def afficher(): """Modifie l'affichage 30 fois par seconde""" pyxel.cls(0) xp = personnage['x'] yp = personnage['y'] pyxel.rect(xp, yp, 4, 8, 1) pyxel.rect(30, 34, 40, 4, 10) pyxel.rect(50, 75, 60, 20, 9) pyxel.init(128, 128, title="Mon premier jeu") # Initialisation de la fenêtre pyxel.run(controler, afficher) # Alternance controler/afficher

Nous allons maintenant voir comment écrire les documentations de vos fonctions personnelles.

(Rappel) 3.3 DOCUMENTATION rapide : le prototype + une phrase
1 2 3
def addition(nbr1:int, nbr2:int) -> int: """Renvoie la somme de nbr1 et nbr2""" return nbr1 + nbr2

Rappel : nous avions déjà vu la signature d'un opérateur (comme +) : int + int -> int

A - Signature d'une fonction
Arguments  ⇒   Fonction   ⇒  Réponse
La signature d'une fonction permet de connaître l'ordre et le type des arguments et de la sortie lors d'un appel de fonction. Elle comporte :

  1. le nom de la fonction ;
  2. l'ordre et le type des arguments ;
  3. le type de la réponse.

Exemple :

addition(int, int) -> int

B - Prototype d'une fonction
Le prototype d'une fonction est l'ensemble de la signature et des noms des paramètres qui vont recevoir les arguments de l'appel.

Exemple avec la fonction addition() :

addition(nombre1:int, nombre2:int) -> int

BONNE PRATIQUE

Le prototype ne contient en soi aucune indication sur ce que réalise la fonction MAIS, bonne pratique, on utilise toujours des noms permettant de s'en douter :

addition(nombre1:int, nombre2:int) -> int    ✅

g(a:int, b:int) -> int    ❌

En Python, on peut donc dire que la première ligne de la déclaration d'une fonction fournit ce prototype.

C - Spécification : la documentation minimale d'une fonction
La spécification d'une fonction regroupe le prototype et une explication textuelle ou mathématique de ce qu'on a le droit de demander à la fonction et ce qu'elle va alors répondre.
En Python, la phrase doit être un string multiligne situé directement sous la déclaration ( donc avec trois guillemets d'ouverture et de fermeture).

On l'appelle DOCSTRING, pour Documentation String.

Exemple

1 2 3
def addition(nb1:int, nb2:int) -> int: """Renvoie la somme de nb1 et nb2""" return nb1 + nb2

L'appel help(addition) renvoie alors la documentation tapée :

>>> help(addition) Help on function addition in module __main__: addition(nb1: int, nb2: int) -> int Renvoie la somme de nb1 et nb2

✔ 06-D° Pour gérer encore mieux la gravité, nous allons créer une fonction peut_tomber() qui devra décider si oui ou non le personnage peut tomber. A partir de maintenant, le personnage ne pourra tomber que si tous les pixels sous lui sont de couleur 0. D'ailleurs, puisque nous ne sommes pas certain que le personnage gardera une dimension de 4*8 pixels, nous décidons de devoir fournir à cette fonction la largeur et la hauteur du personnage.

Encore mieux : nous ne sommes même pas certain qu'il n'y aura qu'un personnage. Pourquoi ne pas donner à peut_tomber() la référence du personnage qui nous intéresse et ajouter dans les dictionnaires "personnage" la largeur 'w' et la hauteur 'h' de ce personnage ?

D'ailleurs, puisque nous avons deux personnages à afficher, autant réaliser également une fonction qui servira à afficher le personnage qu'on envoie en argument.

  1. Lancer ce code qui ne fonctionne pas correctement pour le moment puisque la fonction peut_tomber() renvoie toujours False.
  2. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56
    import pyxel import random perso1 = {'x':90, 'y':0, 'w':4, 'h':8} perso2 = {'x':50, 'y':0, 'w':4, 'h':8} def controler() -> None: """Modifie les données 30 fois par seconde""" if peut_tomber(perso1): perso1['y'] = perso1['y'] + 1 if peut_tomber(perso2): perso2['y'] = perso2['y'] + 1 def afficher() -> None: """Modifie l'affichage 30 fois par seconde""" pyxel.cls(0) afficher_perso(perso1) afficher_perso(perso2) afficher_plateformes() def peut_tomber(perso:dict) -> bool: """Renvoie True si tous les pixels sous le personnage sont bien vides, False sinon""" return False def afficher_perso(perso:dict) -> bool: """Affiche le personnage correspondant au paramètre perso""" xp = perso['x'] # x du point en haut à gauche du personnage voulu yp = perso['y'] # y du point en haut à gauche du personnage voulu hauteur = perso['h'] # hauteur du personnage largeur = perso['w'] # largeur du personnage pyxel.rect(xp, yp, largeur, hauteur, 1) def afficher_plateformes() -> bool: """Affiche les plateformes""" pyxel.rect(30, 34, 40, 4, 10) pyxel.rect(50, 75, 60, 20, 9) # Programme principal pyxel.init(128, 128, title="Mon premier jeu") # Initialisation de la fenêtre pyxel.run(controler, afficher) # Alternance controler/afficher
  3. Observer le code de controler() pour comprendre comment fonctionne ce nouveau contrôle du jeu.
  4. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
    import pyxel import random perso1 = {'x':90, 'y':0, 'w':4, 'h':8} perso2 = {'x':50, 'y':0, 'w':4, 'h':8} def controler() -> None: """Modifie les données 30 fois par seconde""" if peut_tomber(perso1): perso1['y'] = perso1['y'] + 1 if peut_tomber(perso2): perso2['y'] = perso2['y'] + 1
  5. Observer le code de afficher() pour comprendre comment fonctionne ce nouvel affichage du jeu.
  6. 19 20 21 22 23 24 25 26
    def afficher() -> None: """Modifie l'affichage 30 fois par seconde""" pyxel.cls(0) afficher_perso(perso1) afficher_perso(perso2) afficher_plateformes()
  7. Observer le code de afficher_perso() pour comprendre comment elle réalise son travail.
  8. 36 37 38 39 40 41 42 43
    def afficher_perso(perso:dict) -> bool: """Affiche le personnage correspondant au paramètre perso""" xp = perso['x'] # x du point en haut à gauche du personnage voulu yp = perso['y'] # y du point en haut à gauche du personnage voulu hauteur = perso['h'] # hauteur du personnage largeur = perso['w'] # largeur du personnage pyxel.rect(xp, yp, largeur, hauteur, 1)
  9. Visualiser le schéma ci-dessous pour être certain de comprendre qui utilise qui.
flowchart TB R([run]) R --> C([controler]) R --> V([afficher]) C --> PT([peut_tomber]) V --> AP([afficher_perso]) V --> APF([afficher_plateformes])

⚙ 06-E° Compléter maintenant le code de la fonction peut_tomber() pour qu'elle fasse son travail. Le principe : un algorithme de recherche linéaire :

  • Tester chaque pixel sous le personnage avec une boucle pour. Si l'un de ces pixels ne contient pas la couleur 0, on sait qu'on peut répondre faux et sortir définitement.
  • Si on va jusqu'au bout de la boucle précédente, on sait qu'on peut répondre vrai : tous les pixels sous ce personnage sont bien vides.
  • Si vous ne parvenez pas à le faire seul, vous pouvez cliquer d'abord sur le code à trou présenté : attention, il ne s'agit que d'une proposition.

    ...CODE A TROU...

    1 2 3 4 5 6 7 8 9 10 11 12 13 14
    def peut_tomber(perso:dict) -> bool: """Renvoie True si tous les pixels sous le personnage sont bien vides, False sinon""" xp = ... # x du point en haut à gauche du personnage voulu yp = ... # y du point en haut à gauche du personnage voulu hauteur = ... # hauteur du personnage largeur = ... # largeur du personnage ligne_en_dessous = ... + ... # On rajoute hp pour atteindre la ligne en dessous for x in range(xp, ...): # Pour chaque des x du personnage if pyxel.pget(..., ...) != 0: # si le pixel en dessous n'est pas vide return ... # on ne peut pas tomber return ... # Si on arrive ici, on peut tomber, toutes les cases sont noires

...CORRECTION...

1 2 3 4 5 6 7 8 9 10 11 12 13 14
def peut_tomber(perso:dict) -> bool: """Renvoie True si tous les pixels sous le personnage sont bien vides, False sinon""" xp = perso['x'] # x du point en haut à gauche du personnage voulu yp = perso['y'] # y du point en haut à gauche du personnage voulu hauteur = perso['h'] # hauteur du personnage largeur = perso['w'] # largeur du personnage ligne_en_dessous = yp + hauteur # On rajoute hp pour atteindre la ligne en dessous for x in range(xp, xp + largeur): # Pour chaque des x du personnage if pyxel.pget(x, ligne_en_dessous) != 0: # si le pixel en dessous n'est pas vide return False # on ne peut pas tomber return True # Si on arrive ici, on peut tomber, toutes les cases sont noires
3.4 DOCUMENTATION longue : le docstring multiligne

A - Limitation de la documentation rapide

Parfois, la documentation rapide ne suffit pas. Par exemple, cette fonction vitesse() :

1 2 3
def vitesse(distance:int, duree:int) -> float: """Renvoie la vitesse connaissant la distance et la durée""" return distance / duree
  1. Problème 1 : on dit juste d'envoyer une durée de type int. Or 0 est un int, mais il provoque une divison par 0. Pas glop.
  2. Problème 2 : aucune indication sur les unités.
B - DOCSTRING multiligne

Pour inclure des informations supplémentaires, on peut fournir une documentation sur plusieurs lignes.

Il n'existe aucune codification imposée pour cette documentation. Voici les éléments qu'elle doit contenir, quelque soit la façon de l'exprimer :

  • SEMANTIQUE : une description de ce que fait la fonction
  • SIGNATURE : l'équivalent de la signature mais sous forme d'un texte plus libre
  • PRECONDITION(S) : une explication claire des conditions supplémentaires sur les paramètres d'entrée si certaines valeurs posent problème.
  • POSTCONDITION(S) : une explication claire de la réponse qui sera fournie si les entrées sont des entrées validant types et préconditions.
  • ...

Une précondition est une condition supplémentaire sur l'entrée que l'utilisateur doit respecter (en plus du simple type) lorsqu'il lance un appel à cette fonction.

Une postcondition est une condition supplémentaire sur la sortie que le développeur s'engage à respecter (en plus du simple type de la sortie). Par contre, le créateur de la fonction n'engage sa responsabilité que si les types et préconditions sur les entrées sont bien validées.

1 2 3 4 5 6 7 8 9
def vitesse(distance, duree): """Renvoie la vitesse connaissant la distance parcourue et la durée :: param distance(int) :: la distance en m :: param duree(int) :: la durée NON NULLE en s :: return (float) :: la vitesse en m.s-1 """ return distance / duree

⚙ 07° Nous voudrions maintenant parvenir à gérer les déplacements gauche et droite des deux personnages.

Le module pyxel permet de surveiller et gérer l'appui sur les touches du clavier à l'aide de la fonction btn() (pour button).

Voici son prototype :

def btn(key:int) -> bool

Elle renvoie bien entendu vrai si on appuie sur la touche indiquée. Quel argument envoyer à key : un entier. Lequel ? L'une des constantes suivantes définies dans le module. Quelques exemples parmi les plus utiles :

  • pyxel.KEY_RETURN pour la touche ENTREE.
  • pyxel.KEY_SPACE pour la barre ESPACE.
  • pyxel.KEY_0 à pyxel.KEY_9 pour les touches de chiffres.
  • pyxel.KEY_A à pyxel.KEY_Z pour les touches de lettres.
  • Pour les touches de flèches :
    • pyxel.DOWN
    • pyxel.UP
    • pyxel.LEFT
    • pyxel.RIGHT

Question

Si nous plaçions ceci dans la fonction controler(), que se passerait-il lors de la prochaine mise à jour de l'affichage ?

1 2
if pyxel.btn(pyxel.KEY_LEFT): perso1['x'] = perso1['x'] - 1

...CORRECTION...

Ce code impose que lorsqu'on détecte un appui sur la touche FLECHE GAUCHE, le personnage 1 doit se déplacer d'un pixel à gauche.

⚙ 08° Voici la nouvelle organisation des fonctions du programme : cette fois, le mouvement d'un personnage est géré par une fonction gerer_mouvement_perso() qui s'occupera de tous les déplacements possibles.

flowchart TB R([run]) R --> C([controler]) R --> V([afficher]) C --> DP([gerer_mouvement_perso]) DP --> PT([peut_tomber]) V --> AP([afficher_perso]) V --> APF([afficher_plateformes])

Pour le moment, la version fournie de gerer_mouvement_perso() est équivalente à ce que vous aviez : le personnage tombe s'il doit tomber.

Question

Quelles sont les touches de gestion du joueur 1 ? du joueur 2 ?

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83
import pyxel import random perso1 = {'x':90, 'y':0, 'w':4, 'h':8} perso2 = {'x':50, 'y':0, 'w':4, 'h':8} def controler() -> None: """Modifie les données 30 fois par seconde""" gerer_mouvement_perso(perso1, pyxel.KEY_LEFT, pyxel.KEY_RIGHT) gerer_mouvement_perso(perso2, pyxel.KEY_A , pyxel.KEY_Z) def afficher() -> None: """Modifie l'affichage 30 fois par seconde""" pyxel.cls(0) afficher_perso(perso1) afficher_perso(perso2) afficher_plateformes() def gerer_mouvement_perso(perso:dict, g:int, d:int) -> None: """Gère les mouvements gauche, droite et chute du personnage reçu :: param perso(dict) :: dictionnaire modélisant un personnage VALIDE :: param g(int) :: un entier correspondant à la touche pour aller à gauche :: param d(int) :: un entier correspondant à la touche pour aller à droite PRECONDITION SUR perso : doit contenir des clés nécessaires avec des valeurs correctes Exemple : perso1 = {'x':90, 'y':0, 'w':4, 'h':8} Ce personnage est à placer en (90,0) et a une largeur de 4 pixels et une hauteur de 8 pixels PRECONDITION SUR g et d : il faut transmettre une constante reconnue comme touche par Pyxel REMARQUE : si le personnage atteint y=128, on décide de le replacer en y = 0 De la même façon x devra être bloquée entre 0 et (128-largeur du personnage). """ if peut_tomber(perso): perso['y'] = perso['y'] + 1 def peut_tomber(perso:dict) -> bool: """Renvoie True si tous les pixels sous le personnage sont bien vides, False sinon""" xp = perso['x'] # x du point en haut à gauche du personnage voulu yp = perso['y'] # y du point en haut à gauche du personnage voulu hauteur = perso['h'] # hauteur du personnage largeur = perso['w'] # largeur du personnage ligne_en_dessous = yp + hauteur # On rajoute hp pour atteindre la ligne en dessous for x in range(xp, xp + largeur): # Pour chaque des x du personnage if pyxel.pget(x, ligne_en_dessous) != 0: # si le pixel en dessous n'est pas vide return False # on ne peut pas tomber return True # Si on arrive ici, on peut tomber, toutes les cases sont noires def afficher_perso(perso:dict) -> bool: """Affiche le personnage correspondant au paramètre perso""" xp = perso['x'] # x du point en haut à gauche du personnage voulu yp = perso['y'] # y du point en haut à gauche du personnage voulu hauteur = perso['h'] # hauteur du personnage largeur = perso['w'] # largeur du personnage pyxel.rect(xp, yp, largeur, hauteur, 1) def afficher_plateformes() -> bool: """Affiche les plateformes""" pyxel.rect(30, 34, 40, 4, 10) pyxel.rect(50, 75, 60, 20, 9) # Programme principal pyxel.init(128, 128, title="Mon premier jeu") # Initialisation de la fenêtre pyxel.run(controler, afficher) # Alternance controler/afficher

...CORRECTION...

En lisant la documentation longue de gerer_mouvement_perso(), on voit que les paramètres g et d doivent recevoir les codes des touches pour le déplacement gauche et droite.

9 10 11 12 13 ... ... 26 27 28 29 30 31
def controler() -> None: """Modifie les données 30 fois par seconde""" gerer_mouvement_perso(perso1, pyxel.KEY_LEFT, pyxel.KEY_RIGHT) gerer_mouvement_perso(perso2, pyxel.KEY_A , pyxel.KEY_Z) def gerer_mouvement_perso(perso:dict, g:int, d:int) -> None: """Gère les mouvements gauche, droite et chute du personnage reçu :: param perso(dict) :: dictionnaire modélisant un personnage VALIDE :: param g(int) :: un entier correspondant à la touche pour aller à gauche :: param d(int) :: un entier correspondant à la touche pour aller à droite

En comparant les appels des lignes 12-13 avec le prototype visible en ligne 26, on voit que :

  • Le joueur 1 doit gérer ses mouvements avec les flèches gauche et droite du clavier ;
  • Le joueur 2 doit gérer ses mouvements avec les touches A et B ;

⚙ 09-A° Lire entièrement la documentation fournie pour la fonction gerer_mouvement_perso() et compléter son code pour qu'elle fasse le travail qu'on lui demande.

Ci-dessous la version code à trou si vous ne parvenez pas à avancer seul.

...CODE A TROU...

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
def gerer_mouvement_perso(perso:dict, g:int, d:int) -> None: """Gère les mouvements gauche, droite et chute du personnage reçu :: param perso(dict) :: dictionnaire modélisant un personnage VALIDE :: param g(int) :: un entier correspondant à la touche pour aller à gauche :: param d(int) :: un entier correspondant à la touche pour aller à droite PRECONDITION SUR perso : doit contenir des clés nécessaires avec des valeurs correctes Exemple : perso1 = {'x':90, 'y':0, 'w':4, 'h':8} Ce personnage est à placer en (90,0) et a une largeur de 4 pixels et une hauteur de 8 pixels PRECONDITION SUR g et d : il faut transmettre une constante reconnue comme touche par Pyxel REMARQUE : si le personnage atteint y=128, on décide de le replacer en y = 0 De la même façon x devra être bloquée entre 0 et (128-largeur du personnage). """ # Gestion des chutes if peut_tomber(perso): perso['y'] = perso['y'] + 1 # Gestion des déplacements horizontaux if pyxel.btn(...): # si appuie sur la touche qui gère le déplacement gauche perso['x'] = ... if ... perso['x'] = ... # Gestion des positions maximales if ...['y'] >=...: perso['y'] = 0 if perso['x'] < 0: ... elif perso['x'] >= (128 - perso['w']): ...

...CORRECTION...

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
def gerer_mouvement_perso(perso:dict, g:int, d:int) -> None: """Gère les mouvements gauche, droite et chute du personnage reçu :: param perso(dict) :: dictionnaire modélisant un personnage VALIDE :: param g(int) :: un entier correspondant à la touche pour aller à gauche :: param d(int) :: un entier correspondant à la touche pour aller à droite PRECONDITION SUR perso : doit contenir des clés nécessaires avec des valeurs correctes Exemple : perso1 = {'x':90, 'y':0, 'w':4, 'h':8} Ce personnage est à placer en (90,0) et a une largeur de 4 pixels et une hauteur de 8 pixels PRECONDITION SUR g et d : il faut transmettre une constante reconnue comme touche par Pyxel REMARQUE : si le personnage atteint y=128, on décide de le replacer en y = 0 De la même façon x devra être bloquée entre 0 et (128-largeur du personnage). """ # Gestion des chutes if peut_tomber(perso): perso['y'] = perso['y'] + 1 # Gestion des déplacements horizontaux if pyxel.btn(g): # si appuie sur la touche qui gère le déplacement gauche perso['x'] = perso['x'] - 1 # on déplace à gauche if pyxel.btn(d): # si appuie sur la touche qui gère le déplacement droite perso['x'] = perso['x'] + 1 # on déplace à droite # Gestion des positions maximales if perso['y'] >=128: perso['y'] = 0 if perso['x'] < 0: perso['x'] = 0 elif perso['x'] >= (128 - perso['w']): perso['x'] = (127 - perso['w'])

⚙ 09-B° Si vous lancez le jeu, vous devriez voir qu'on parvient à placer un personnage à l'intérieur d'un bloc en lui faisant percuter le côté d'un bloc de plateforme. A qui la faute ? A la personne qui a codé la fonction ou au chef de projet qui n'a rien dit à ce sujet dans la documentation qu'il a fourni ?

...CORRECTION...

A la personne qui a tapé la documentation.

Cette documentation sert également de description du produit fini avant de le produire.

Une bonne pratique de travail consiste à

  • Ecrire d'abord la documentation ;
  • Coder la fonction ensuite !

C'est tout pour aujourd'hui avec Pyxel mais vous aurez l'occasion de le découvrir à l'aide d'un projet qu'il faudra réaliser et de la nuit du code en mai pour ceux qui le veulent : 6h de code en équipe pour produire le jeu vidéo le plus abouti possible.

Il reste par contre encore quelques notions à découvrir ou à utiliser sur les fonctions.

⚙ 10° Voici la fonction calculer_moyenne() en version documentation multiligne. Répondre aux questions suivantes :

  1. Sur quelles lignes trouve-t-on les types attendus des deux paramètres ?
  2. Quelles sont les préconditions qu'on trouve dans cette documentation plus longue ?
  3. Sur quelle ligne se trouve l'indication sur le type de la réponse ?
  4. Y-a-t-il une postcondition indiquée ?
1 2 3 4 5 6 7 8 9 10 11 12
def calculer_moyenne(note1, note2): """Renvoie la moyenne des deux notes :: param note1(int) :: une note dans [0;20] :: param note2(int) :: une note dans [0;20] :: return (float) :: la moyenne de note1 et note2, dans [0;20] """ return (note1 + note2) / 2 m = calculer_moyenne(10, 20) print(m)

Enfin, lancer le programme dans Thonny pour vérifier que la fonction fonctionne de la même façon que celle de la question précédente mais que la documentation est juste plus fournie lorsqu'on utilise help().

>>> help(calculer_moyenne) Help on function calculer_moyenne in module __main__: calculer_moyenne(note1, note2) Renvoie la moyenne des deux notes :: param note1(int) :: une note dans [0;20] :: param note2(int) :: une note dans [0;20] :: return (float) :: la moyenne de note1 et note2, dans [0;20]

...CORRECTION...

  1. types attendus
  2. préconditions
  3. le type de la réponse
  4. postcondition
1 2 3 4 5 6 7 8 9 10 11 12
def calculer_moyenne(note1, note2): """Renvoie la moyenne des deux notes :: param note1(int) :: une note dans [0;20] :: param note2(int) :: une note dans [0;20] :: return (float) :: la moyenne de note1 et note2, dans [0;20] """ return (note1 + note2) / 2 m = calculer_moyenne(10, 20) print(m)

⚙ 11° Placer la fonction ci-dessous en mémoire.

  1. Réaliser les appels proposés pour comprendre ce que fait cette fonction qui ne comporte aucune documentation utile.
  2. Ecrire ensuite la documentation multiligne.
  3. Copier votre fonction et remplacer la documentation par une version rapide (prototype et une seule ligne en français).
1 2 3 4 5 6 7 8 9 10
import random def de(nb_faces): '''La petite phrase qui va bien :: à faire : décrire le paramètre :: :: à faire : décrire la réponse :: ''' return random.randint(1, nb_faces)
>>> de(6) 5 >>> de(6) 6 >>> de(6) 3 >>> de(6) 4 >>> de(20) 18 >>> de(20) 3 >>> de(20) 20 >>> de(20) 7
>>> import random >>> help(random.randint) Help on method randint in module random: randint(a, b) method of random.Random instance Return random integer in range [a, b], including both end points.

...CORRECTION...

1 2 3 4 5 6 7 8 9 10
import random def de(nb_faces): '''Renvoie un nombre aléatoire dans [1; nb_faces] :: nb_faces(int) :: valeur maximale voulue pour le dé :: return (int) :: un entier aléatoire dans [1; nb_faces] ''' return random.randint(1, nb_faces)
1 2 3 4 5
import random def de(nb_faces:int) -> int: '''Renvoie un nombre aléatoire dans [1; nb_faces]''' return random.randint(1, nb_faces)

⚙ 12° Réaliser la fonction telle qu'elle est spécifiée dans la documentation.

1 2 3
def est_long(chaine:str) -> bool: """Fonction qui renvoie True si la chaine est d'au moins 6 caractères, False sinon""" pass

Une fonction sans instruction provoque une erreur. L'instruction pass est une instruction qui veut dire "Ne fait rien". On place donc souvent pass comme instruction temporaire.

...CORRECTION...

1 2 3
def est_long(chaine:str) -> bool: """Fonction qui renvoie True si la chaine est d'au moins 6 caractères, False sinon""" return len(chaine) >= 6
3.5 Contrat de confiance

Contrat de confiance

Une précondition est une condition supplémentaire sur l'entrée que l'utilisateur doit respecter lorsqu'il lance un appel à cette fonction.

Une postcondition est une condition supplémentaire sur la sortie que le développeur s'engage à respecter. Par contre, le créateur de la fonction n'engage sa responsabilité que si les types et préconditions sur les entrées sont bien validées.

En informatique, le contrat de confiance entre les utilisateur et concepteurs est basé sur les principes suivants :
  • Les utilisateurs s'engagent à respecter les types et préconditions sur les entrées ;
  • Les concepteurs s'engagent à garantir les types et postconditions des sorties.
Premier cas : l'utilisateur respecte les préconditions

L'utilisateur peut concevoir le reste de son programme en considérant que la postcondition est vraie : le concepteur de la fonction s’y engage.

Si la fonction provoque une erreur dommageable, c'est la faute du concepteur car l'utilisateur a envoyé des données respectant types et préconditions.

Deuxième cas : l'utilisateur ne respecte pas les préconditions

Si le programme provoque un dommage, c'est clairement la faute de l'utilisateur : il a utilisé la fonction sans respecter la documentation.

Equation mathématique du contrat de confiance : l'implication

Si on résume :

[Contrat de confiance]
  • SI l'utilisateur respecte les préconditions, ALORS le concepteur garantit les postconditions.
  • SI l'utilisateur ne respecte pas les préconditions, ALORS le concepteur ne garantit rien sur la réponse.

On peut résumer le contrat de confiance par cette simple équation mathématique :

entrées valides sortie valide

types et préconditions type et postconditions sur la sortie

C'est pour cela que la documentation est si importante : en plus de garantir le bon fonctionnement, elle permet d'établir les responsabilités de chacun en cas de problème. Lorsque vous venez de perdre 8 millions d'euros à cause d'un "bug" informatique, il est toujours bon de savoir qui l'a provoqué.

3.6 3 étages de confiance

Le contrat de confiance comporte 3 étages :

  1. Le chef de projet établit juste la documentation et quelques exemples d'utlisation, sans réaliser le code interne. Cela permet de définir clairement ce qu'il veut. Si vous codifier ainsi vos demandes à une IA générative, elle pourra sans doute vous proposer une version correcte de la fonction.
  2. Le développeur-concepteur réalise un code interne qui doit respecter ce qui est noté dans la documentation et les exemples d'utlisation. Dans les années futurs, ce code sera certainement de plus en plus fourni par une IA. Il faudra sans doute encore faire appel aux humains s'il y a de vraies subtilités à gérer.
  3. Le développeur-utilisateur utilise la fonction en respectant les contraintes que lui impose la documentation sur les arguments qu'il a le droit d'envoyer.

Cela veut dire que

  • si le concepteur réalise une fonction en respectant bien la documentation créée par le chef de projet,
  • si cette fonction est correctement réalisée
  • mais qu'elle pose problème sur un appel totalement légitime au vu de la documentation fournie, le problème vient du chef de projet : il a oublié de spécifier certaines particularités qu'il aurait fallu gérer.

4 - Return, ou pas Return, sortez !

Que se passe-t-il lorsque la fonction rencontre un return ?

A quel moment sort-on vraiment de la fonction ?

⚙ 13° On a demandé à quelqu'un de compléter la fonction suivante pour qu'elle renvoie le chiffre des centaines d'un nombre envoyé en argument. Observer la fonction centaine() : il a tapé ceci en oubliant de supprimer la ligne 8, celle qui contenait une réponse fictive mais du bon type.

1 2 3 4 5 6 7 8 9 10
def centaine(x): """Renvoie le chiffre de la centaine de x :: param x(int) :: un entier :: return (int) :: la centaine de x (5 si 1545) """ return 0 mon_petit_calcul = x // 100 % 10 return mon_petit_calcul

Tester la fonction dans la console.

Questions

  1. Pourquoi cette fonction renvoie-t-elle TOUJOURS 0 ?
  2. Modifier la fonction pour qu'elle fonctionne correctement.

Si vous ne voyez pas, Python Tutor est votre ami.

...CORRECTION...

On aurait pu croire que la fonction suit son cours et finit par répondre à la fin. Mais non !

Dès qu'on rencontre un return, la fonction renvoie ce qu'on a placé derrière et on sort immédiatement.

Dès qu'on arrive sur la ligne 8, on renvoie 0. Et on sort de la fonction.

Les lignes 09 et 10 ne servent donc strictement à rien puisque l'interpréteur ne va jamais les rencontrer : il sort avant. C'est comme si vous aviez tapé ceci :

1 2 3 4 5 6 7 8 9 10
def centaine(x): """Renvoie le chiffre de la centaine de x :: param x(int) :: un entier :: return (int) :: la centaine de x (5 si 1545) """ return 0

Après correction, vous devriez avoir ceci :

1 2 3 4 5 6 7 8
def centaine(x): """Renvoie le chiffre de la centaine de x :: param x(int) :: un entier :: return (int) :: la centaine de x (5 si 1545) """ return x // 100 % 10
4.1 FONCTION : sortie immédiate avec return

Dès que la fonction rencontre un return :

  1. elle évalue l'expression fournie derrière return ;
  2. elle renvoie sa réponse à l'endroit d'où l'appel a été fait ;
  3. son espace local des noms est détruit définitivement.

C'est ce mécanisme qui nous a permis de réaliser l'algorithme de recherche dans un tableau en utilisant un for.

1 2 3 4 5 6 7
def rechercher(t, x): """Renvoie l'indice de la première occurrence de x dans t, -1 si non présent""" for i in range( len(t) ): if t[i] == x: return i return -1

Lorsqu'on rencontre le return, on sort immédiatement. Ok.

Mais...

Et si on ne rencontrait jamais de return ? Que se passe-t-il ?

⚙ 14° Observer la fonction suivante qui possède une possibilité de ne jamais rencontrer de return : le concepteur a oublié de noter le return -1 final.

Dans cette version, si on recherche une valeur qui n'existe pas, nous ne validerons jamais la condition de la ligne 5 pour atteindre la ligne 6 et on va donc sortir de la boucle sans rencontre de return. Or, c'est la fin de la fonction...

1 2 3 4 5 6 7 8 9 10 11 12
def rechercher(t:list, x:'Elément') -> int: """Renvoie l'indice de la première occurrence de x dans t, -1 si non présent""" for i in range( len(t) ): if t[i] == x: return i # fin de la fonction tab = ["B", "C", "D"] reponse = rechercher(tab, "A") print(f"A est positionné en {reponse}") print(f"Le type de la réponse est {type(reponse)}")

Lancer le code puis répondre aux questions.

Questions

  1. Que contient la variable reponse ?
  2. Quel est le type de cette variable ?
  3. Que semble rajouter l'interpréteur Python dans le code de la fonction si on sort de celle-ci sans rencontrer de renvoi ?

...CORRECTION...

La variable contient bien quelque chose alors que la fonction n'a rien répondu.

Elle contient None, ce qui pourrait se traduire en "Rien" ou "Vide".

Le type de None est NoneType.

A est positionné en None Le type de la réponse est <class 'NoneType'>

Dernier point : si vous écrivez une fonction qui ne renvoie rien, c'est comme si votre dernière instruction était return None.

1 2 3 4 5 6 7 8 9 10 11 12
def rechercher(t:list, x:'Elément') -> int: """Renvoie l'indice de la première occurrence de x dans t, -1 si non présent""" for i in range( len(t) ): if t[i] == x: return i return None # fin de la fonction tab = ["B", "C", "D"] reponse = rechercher(tab, "A") print(f"A est positionné en {reponse}") print(f"Le type de la réponse est {type(reponse)}")
4.2 FONCTION : return None

A - La valeur None
La valeur particulière notée None correspond à une absence d'informations. Elle est la seule valeur possible du type natif NoneType.
Lors d'un transtypage vers bool, la valeur None est considérée comme le cas nul ou vide.
>>> bool(None) False

Attention néanmoins, None n'est identique à False, c'est uniquement lorsqu'on veut le transtyper en booléen qu'on obtient cette valeur équivalente.

>>> None == False False >>> bool(None) == False True
B - Le cas du return absent dans une fonction
Une fonction Python renvoie au moins la valeur None lorsqu'elle se termine sans avoir rencontré un autre return.
La ligne 4 ci-dessous est totalement facultative et les deux fonctions sont strictement équivalentes :

1 2 3 4
def salutation(nom:str) -> None: """Fonction qui affiche bonjour à la personne""" print(f"Bonjour {nom} !") return None
1 2 3 4
def salutation(nom:str) -> None: """Fonction qui affiche bonjour à la personne""" print(f"Bonjour {nom} !")
Dans certains langages de programmation, une procédure est le nom des routines qui agissent mais ne renvoient réellement aucune valeur. Python ne possède pas de véritables procédures mais, par abus de langage, on appelle parfois "procédure" une fonction Python qui renvoie uniquement et toujours None.
C - print() n'est pas un return
HYPER IMPORTANT : attention à la différence entre les fonctions qui affichent et les fonctions qui renvoient une valeur.

Fonction qui renvoie

Si on vous demande de créer une fonction qui renvoie, il faut utiliser un return. On pourra stocker la valeur.

1 2 3 4 5
def fois2(v:int) -> int: """Renvoie le double de la valeur envoyée""" return v * 2 rep = fois2(10)

Si on demande d'observer rep dans la console après exécution du programme, on obtient ceci :

>>> rep 20

Fonction qui affiche

Si on vous demande de créer une fonction qui affiche, il faut utiliser print(). On ne pourra pas stocker récupérer une valeur via cette fonction car, justement, print() ne renvoie aucune valeur : elle ne fait qu'afficher à l'écran. Inutile donc de tenter de stocker la réponse de la fonction, elle répond None dans tous les cas.

1 2 3 4 5
def fois2(v:int) -> int: """Affiche le double de la valeur envoyée""" print(v * 2) fois2(10)

L'erreur terrible

Que se passe-t-il si vous confondez les deux ? Et bien, vous allez droit dans le mur. Un exemple avec le code ci-dessous où on tente de mémoriser le "résultat" de la fonction.

1 2 3 4 5
def fois2(v:int) -> int: """??? le double de la valeur envoyée""" print(v * 2) rep = fois2(10)

Que renvoie fois2() ? None !

Moralité : la personne qui a tapé cela ne comprend pas ce qu'il fait puisque rep contiendra donc toujours None.

>>> rep >>>

5 - [Hors programme] Compléments sur les paramètres

Cette partie ne comporte aucun attendu du programme de NSI.

Par contre, elle vous permettra de mieux comprendre certains codes que vous pourriez trouver sur le Web et vous simplifier la vie lors de vos projets.

5.1 FONCTION : Paramètres par défaut

5.1.1 Intérêt

Imaginons une fonction qui renvoie un résultat aléatoire compris entre un entier debut et un entier fin :

1 2 3 4 5 6 7 8 9 10 11
import random def aleatoire(debut, fin): """Renvoie un entier aléatoire entre debut et fin :: param debut(int) :: un entier :: param fin(int) :: un entier SUPERIEUR à debut :: return (int) :: un entier dans [debut, fin] """ return random.randint(debut, fin)

Si cette fonction doit simuler un dé, l'entier debut sera toujours 1. Et pourtant, il faut toujours lui fournir cette valeur puisqu'il y a deux arguments à envoyer.

Si on ne met qu'un seul paramètre attendu et qu'on impose que la valeur de départ est toujours 1, on obtient une fonction moins flexible.

Comment avoir le meilleur des deux mondes ?

5.1.2 Paramètres par défaut

On peut donner des valeurs d'arguments par défaut aux fonctions. Si l'utilisateur ne fournit pas de valeurs pour ces paramètres, il sera rempli automatiquement avec la valeur située un signe = dans la déclaration.

1 2 3 4 5 6 7 8 9 10 11
import random def aleatoire(fin, debut=1): """Renvoie un entier aléatoire entre debut et fin :: param fin(int) :: un entier SUPERIEUR à debut :: param debut(int) :: un entier :: return (int) :: un entier dans [debut, fin] """ return random.randint(debut, fin)

On peut alors faire des appels en envoyant la valeur de départ, ou pas.

>>> aleatoire(50, 40) 43 >>> aleatoire(50) 12

Attention au positionnement : remarquez bien qu'il faut que les paramètres ayant une valeur par défaut soient positionnés derrière les paramètres sans valeur par défaut. C'est pour cela que debut=1 est déclarée derrière fin.

5.1.3 Documentation

On peut bien entendu continuer à indiquer rapidement le type des paramètres, même avec des valeurs par défaut.

1 2 3 4 5
import random def aleatoire(debut:int=1, fin:int=1): """Renvoie un entier aléatoire entre debut et fin""" return random.randint(debut, fin)
5.2 FONCTION : Paramètres nommés ou positionnels

5.2.1 Paramètres nommés

Cette fonctionnalité est présente de base dans Python : plutôt que de fournir les arguments dans l'ordre imposé par l'ordre des paramètres dans la déclaration, on peut les fournir comme on le veut, pourvu de tous les fournir.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
import random def somme_aleatoire(nb, debut, fin): """Renvoie la somme de x lancés aléatoire entre debut et fin :: param nb(int) :: POSITIF, le nombre de lancés voulus :: param debut(int) :: un entier :: param fin(int) :: un entier SUPERIEUR à debut :: return (int) :: un entier dans [nb*debut, nb*fin] """ s = 0 # Compteur-mémoire de la somme for _ in range(nb): # Fait l'action nb fois s = s + random.randint(debut, fin) # Incrémente s d'un nombre aléatoire return s
>>> somme_aleatoire(10, 1, 6) # on demande 10 dés à 6 faces >>> somme_aleatoire(nb=10, debut=1, fin=6) # on demande 10 dés à 6 faces >>> somme_aleatoire(debut=1, fin=6, nb=10) # on demande 10 dés à 6 faces
5.2.2 Paramètres positionnels uniquement

Nommer les paramètres, c'est pratique mais dangereux. Un changement de nom et tous les programmes utilisant l'ancienne version ne fonctionneront plus.

On peut empêcher l'utilisation des paramètres nommés et donc imposer que seul l'ordre de transfert des arguments soit important.

Pour cela, il suffit de rajouter un symbole / dans la liste des paramètres.

1 2 3 4
import random def somme_aleatoire(nb, /, debut, fin): ...
  • Les paramètres à gauche du / sont positionnels uniquement (nb).
  • Les paramètres à droite du / sont nommables lors de l'appel (debut et (fin)).
>>> somme_aleatoire(10, 1, 6) # on demande 10 dés à 6 faces >>> somme_aleatoire(10, debut=1, fin=6) # on demande 10 dés à 6 faces >>> somme_aleatoire(nb=10, debut=1, fin=6) # ECHEC : on tente de nommer le premier paramètre ! TypeError: somme_aleatoire() got some positional-only arguments passed as keyword arguments: 'nb'
5.3 FONCTION : documentation des types construits

Selon les versions de Python, il est possible, ou pas, d'écrire ce que contient un type construit.

Exemple avec une fonction dont le paramètre doit être un tableau d'entiers.

1 2
def f(x:list[int]): ...

Pour un dictionnaire dont les clés sont des strings et les valeurs sont des integers :

1 2
def f(x:dict[(str, int)]): ...

Si la version de Python que vous utilisez provoque une erreur et que vous ne pouvez pas en changer pour une raison ou une autre, il suffit de modifier la documentation pour fournir qu'un string :

1 2
def f(x:"dict[(str, int)]"): ...

6 - FAQ

Rien pour le moment

C'est tout en terme de connaissances sur les fonctions pour aujourd'hui. Si on récapitule, nous avons vu :

  • Comment déclarer une fonction
  • Comment documenter un peu les fonctions
  • Comment une fonction parvient à renvoyer quelque chose avec le mot-clé return
  • Qu'on sort IMMEDIATEMENT de la fonction dès qu'on rencontre return
  • Qu'une fonction-python renvoie au moins None.

Activité publiée le 28 08 2019
Dernière modification : 15 07 2023
Auteur : ows. h.