python fonction et tableaux

Identification

Infoforall

13 - Fonctions et mutables


Dans Python 6 (variables), nous avons vu qu'il existe des variables globales et des variables locales. Or, une fonction ne peut pas modifier une variable globale que ce soit un type simple ou un type structuré.

Pourtant, dans Python 12 (fonctions et variables), nous avons vu qu'une fonction peut modifier le contenu d'un argument-conteneur mutable.

Nous allons étudier cela un peu plus en détails en changeant de mutables : intéressons-nous aux tableaux en plus des dictionnaires.

Exercices supplémentaires 🏠 : sur le site

Logiciel nécessaire pour l'activité : Thonny ou juste Python 3

Evaluation ✎ : questions 05-09-10-11-15-16

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 - Modifier l'original ou modifier une copie

1.1 FONCTION : paramètre référençant un conteneur mutable

LECTURE d'un argument mutable : POSSIBLE

Une fonction peut accéder aux cases d'un conteneur stocké dans un paramètre.

Tableau

1 2 3 4 5
def lecture(t:list, i:'indice') -> int: return t[i] a = [20, 10, 30] print( lecture(a, 2) )
30

Dictionnaire

1 2 3 4 5
def lecture(d:dict, c:'clé') -> int: return d[c] a = {'alice':20, 'bob':10, 'charlie':30} print( lecture(a, 'charlie') )
30
Modification du CONTENEUR mutable : IMPOSSIBLE

La fonction ne peut pas modifier le conteneur-global du programme principal.

Tableau

1 2 3 4 5 6
def modifie(t:list) -> None: t = [100, 100] a = [20, 10, 30] modifie(a) print(a)
[20, 10, 30]

Dictionnaire

1 2 3 4 5 6
def modifie(d:dict) -> None: d = {'alice':100, 'bob':100, 'charlie':100} a = {'alice':20, 'bob':10, 'charlie':30} modifie(a) print(a)
{'alice':20, 'bob':10, 'charlie':30}
Modification du CONTENU : POSSIBLE

La fonction peut modifier le contenu d'une case d'un conteneur-global stocké dans un paramètre (si le conteneur est mutable).

Notez bien qu'on ne modifie pas le conteneur lui-même.

Tableau

1 2 3 4 5 6 7
def mise_a_zero(t:list) -> None: for i in range(len(t)): t[i] = 0 a = [20, 10, 30] mise_a_zero(a) print(a)
[0, 0, 0]

Le tableau global a bien été modifié par la fonction par le parcours par indices. Attention, le parcours par valeurs ne permet pas la modification.

Dictionnaire

1 2 3 4 5 6 7
def mise_a_zero(d:dict) -> None: for c in d.keys(): d[c] = 0 a = {'alice':20, 'bob':10, 'charlie':30} mise_a_zero(a) print(a)
a = {'alice':0, 'bob':0, 'charlie':0}

Le dictionnaire global a bien été modifié par la fonction via un parcours par clés. Attention, le parcours par valeurs ne permet pas la modification.

Pourquoi est-ce que cela fonctionne ?

L'explication arrivera plus loin dans l'activité. Pour le moment, retenons qu'en Python :

⚙ 01-A° Réaliser une fonction qui reçoit un tableau et qui modifie toutes ses cases en les divisant par deux (en utilisant une division euclidienne). On utilisera un parcours par indices puisqu'on désire MODIFIER le contenu.

1 2 3 4 5 6 7 8 9 10 11
def divise_par_2(t:list) -> None: pass a = [10, 20, 30] print("a avant action de la fonction :") print(a) divise_par_2(a) print("a après action de la fonction :") print(a)

Voici le résultat attendu dans la console :

a avant action de la fonction : [10, 20, 30] a après action de la fonction : [5, 10, 15]

...CORRECTION...

1 2 3
def divise_par_2(t:list) -> None: for i in range(len(t)): t[i] = t[i] // 2

⚙ 01-B° Deux questions :

  1. La fonction divise_par_2() renvoie-t-elle quelque chose qui pourrait justifier la modification du contenu ?
  2. Quel est le coût de votre fonction ?

...CORRECTION...

  1. Non, la fonction ne renvoie rien. Le contenu du tableau est bien modifié directement depuis la fonction.
  2. On notera 𝑛 le nombre de cases du tableau.
  3. 1 2 3
    def divise_par_2(t:list) -> None: for i in range(len(t)): # Cette boucle est réalisée n fois t[i] = t[i] // 2 # Cette ligne est à coût constant

    Rappel ; en Python, len() est à coût constant.

  4. On fait 𝑛 fois une action constante. C'est donc un coût linéaire.

⚙ 02° Analyser les instructions internes de la fonction divise_par_2_bis() ci-dessous. Fait-elle la même chose que la fonction divise_par_2() précédente ? En quoi le prototype est-il clair à ce propos ?

1 2 3
def divise_par_2(t:list) -> None: for i in range(len(t)): t[i] = t[i] // 2
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
def divise_par_2(t:list) -> list : copie = [v for v in t] for i in range(len(copie)): copie[i] = copie[i] // 2 return copie a = [10, 20, 30] print("a avant action de la fonction :") print(a) b = divise_par_2(a) print("a après action de la fonction :") print(a) print("b après action de la fonction :") print(b)
a avant action de la fonction : [10, 20, 30] a après action de la fonction : [10, 20, 30] b après action de la fonction : [5, 10, 15]

...CORRECTION...

Cette fois, on renvoie un NOUVEAU tableau. Il est basé sur celui qu'on renvoie en entrée et contient deux fois plus dans toutes les cases.

La nuance ? Le tableau reçu en paramètre n'est pas modifié.

D'ailleurs, la documentation rapide des types indique clairemnet que la fonction va renvoyer un type list.

⚙ 03° Encore mieux que la fonction précédente, on peut créer la copie directement et la renvoyer.

1 2
def divise_par_2(t:list) -> list : return [v//2 for v in t]

Questions

  1. Un utilisateur pourrait-il voir que le code interne des deux fonctions des questions 2 et 3 ne sont pas identiques ?
  2. Comment se nomme la façon de créer la copie de la ligne 2 ?
  3. Traduire la signification de l'expression derrière le return.

...CORRECTION...

  1. Les deux fonctions des questions 2 et 3 renvoient la même sortie. L'implémentation interne est différente, le résultat est identique. L'utilisateur ne pourra se rendre compte de rien, surtout que les deux fonctions ont un coût qui reste linéiare.
  2. La méthode de création se nomme création par compréhension.
  3. Crée un nouveau tableau qui, pour chaque valeur contenue dans t, possède une case qui contient la moitié de cette valeur.

⚙ 04° Créer la fonction ci-dessous : elle doit renvoyer une copie du tableau fourni mais en mettant 0 dans toutes les cases inférieures à 10 et 20 dans toutes les cases supérieures ou égales à 10.

1 2
def super_ou_zero(t:list) -> list : return []

Tester votre fonction à l'aide de quelques tests.

>>> t = [17, 15, 8, 9, 10] >>> t2 = super_ou_zero(t) >>> t2 [20, 20, 0, 0, 20]

...CORRECTION...

Deux versions possibles. Il existe bien d'autres façons de faire cela.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
def super_ou_zero(t:list) -> list : copie = [v for v in t] for i in range(len(copie)): # On doit passer par l'indice car MODIF if copie[i] < 10: copie[i] = 0 else: copie[i] = 20 return copie def super_ou_zero_v2(t:list) -> list : return [f(v) for v in t] def f(x): if x < 10: return 0 else: return 20

⚙ 05° Créer la fonction ci-dessous : elle doit modifier en place le tableau de notes fournies : aucune ne doit dépasser 20.

1 2
def maxi_20(t:list) -> None : pass

Tester votre fonction à l'aide de quelques tests.

>>> t = [27, 25, 8, 9, 17] >>> maxi_20(t) >>> t [20, 20, 8, 9, 17]

...CORRECTION...

1 2 3 4
def maxi_20(t:list) -> None : for i in range(len(t)): if t[i] > 20: t[i] = 20
1.2 Fonction : modifier un tableau

Pour modifier un tableau depuis une fonction : il faut passer par les indices.

1 2 3 4 5 6
def modifier(t): for i in range( len(t) ): t[i] = t[i] ** 2 notes = [15, 18, 8, 10, 12, 15, 20, 5, 12, 17, 12, 10, 18, 4] modifier(notes)
1.3 Fonction : renvoyer une copie d'un tableau

Pour renvoyer un nouveau tableau basé sur un tableau-paramètre depuis une fonction : une seule méthode utilisable

  • Créer une copie du tableau initial
  • Modifier la copie en utilisant les indices
  • Renvoyer la copie (et la stocker dans une variable !)
1 2 3 4 5 6 7 8
def copie_modifiee(t): copie = [v for v in t] for i in range( len(copie) ): copie[i] = copie[i] * 2 return copie notes = [15, 18, 8, 10, 12, 15, 20, 5, 12, 17, 12, 10, 18, 4] nouvelles_notes = copie_modifiee(notes)

Dans la plupart des cas, la création par compréhension peut résoudre le problème posé en une ligne :

1 2 3 4 5
def copie_modifiee(t): return [v*2 for v in t] notes = [15, 18, 8, 10, 12, 15, 20, 5, 12, 17, 12, 10, 18, 4] nouvelles_notes = copie_modifiee(notes)

2 - Application à Pyxel : jeu de poursuite

Nous allons profiter de cette modification possible des types construits mutables en les utilisant dans nos interfaces Pyxel.

Nous allons aujourd'hui utiliser à nouveau notre plateforme et réaliser un jeu où il faudra fuir votre ou vos poursuivants.

Poursuivant qui seront gérés par l'ordinateur.

✔ 06-A° Dans la version précédente de notre jeu, les plateformes étaient juste générées à l'affichage mais leurs coordonnées exactes n'étaient pas accessibles directement. C'est une mauvaise pratique de programmation : normalement, nous tentons toujours de séparer les données et l'affichage en lui-même.

Utiliser ce nouveau programme.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129
import pyxel import random perso1 = {'x':90, 'y':0, 'w':4, 'h':8, 'c':1} # joueur 1 perso2 = {'x':50, 'y':0, 'w':4, 'h':8, 'c':2} # joueur 2 p1 = [30, 35, 40, 4, 0] # [x, y, largeur, hauteur, déplacement] p2 = [60, 55, 30, 4, 0] p3 = [50, 75, 60, 4, 0] p4 = [10, 95, 60, 4, 0] plateformes = [p1, p2, p3, p4] def controler() -> None: """Modifie les données 30 fois par seconde""" gerer_mouvement_perso(perso1, pyxel.KEY_LEFT, pyxel.KEY_RIGHT, pyxel.KEY_UP) gerer_mouvement_perso(perso2, pyxel.KEY_A, pyxel.KEY_Z, pyxel.KEY_SPACE) def afficher() -> None: """Modifie l'affichage 30 fois par seconde""" pyxel.cls(0) afficher_perso(perso1) afficher_perso(perso2) gerer_mouvement_plateformes() afficher_plateformes() def gerer_mouvement_robot(robot:dict, cible:dict) -> None: """Modifie les coordonnées du robot en fonction de celles de la cible""" pass def gerer_mouvement_plateformes() -> None: """Modifie aléatoirement l'abscisse des plateformes""" pass def gerer_mouvement_perso(perso:dict, g:int, d:int, s: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 :: param s(int) :: un entier correspondant à la touche pour sauter 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, d, s : 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 gauche perso['x'] = perso['x'] - 1 # on déplace à gauche if pyxel.btn(d): # si appuie sur la touche droite perso['x'] = perso['x'] + 1 # on déplace à droite if pyxel.btn(s) and not peut_tomber(perso): # si appuie sur la touche saut en étant sur une plateforme perso['y'] = perso['y'] - 25 # on saute # 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']) 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 couleur = perso['c'] # largeur du personnage pyxel.rect(xp, yp, largeur, hauteur, couleur) def afficher_plateformes() -> bool: """Affiche les plateformes""" for plateforme in plateformes: x = plateforme[0] # Colonne y = plateforme[1] # Ligne w = plateforme[2] # Largeur / width h = plateforme[3] # Hauteur / height pyxel.rect(x, y, w, h, 10) # Programme principal pyxel.init(128, 128, title="Mon premier jeu") # Initialisation de la fenêtre pyxel.run(controler, afficher) # Alternance controler/afficher

⚙ 06-B° Isolons mentalement les lignes qui nous intéressent : celles gérant les plateformes.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
p1 = [30, 35, 40, 4, 0] # [x, y, largeur, hauteur, déplacement] p2 = [60, 55, 30, 4, 0] p3 = [50, 75, 60, 4, 0] p4 = [10, 95, 60, 4, 0] plateformes = [p1, p2, p3, p4] def afficher() -> None: """Modifie l'affichage 30 fois par seconde""" ... afficher_plateformes() def afficher_plateformes() -> bool: """Affiche les plateformes""" for plateforme in plateformes: x = plateforme[0] # Colonne y = plateforme[1] # Ligne w = plateforme[2] # Largeur / width h = plateforme[3] # Hauteur / height pyxel.rect(x, y, w, h, 10)
  1. Quelle est l'épaisseur des 4 plateformes ?
  2. Que renvoie l'évaluation de plateformes[0] ?
  3. Le parcours visible dans la fonction afficher_plateformes() est-il un parcours par indices ou un parcours par valeurs ?
  4. Expliquer comment la fonction afficher_plateformes() parvient à afficher les plateformes à partir des données stockées dans la variable globale plateformes.

...CORRECTION...

  1. La hauteur des plateformes (l'épaisseur) est stockée dans chaque tableau sur l'indice 3, la dernière case : l'épaisseur est de 4.
  2. 1 2 3 4 5
    0 1 2 3 4 p1 = [30, 35, 40, 4, 0] # [x, y, largeur, hauteur, déplacement] p2 = [60, 55, 30, 4, 0] p3 = [50, 75, 60, 4, 0] p4 = [10, 95, 60, 4, 0]
  3. plateformes[0] renvoie le premier élément de ce tableau, à savoir le tableau p1.
  4. 1 2
    0 1 2 3 plateformes = [p1, p2, p3, p4]
  5. Le parcours visible est un parcours par valeurs : la variable plateforme va donc référencer une par une chacune des variables p1 p2 p3 p4.
  6. Voici la traduction de chacune des lignes :
    • L16 : Pour chaque plateforme dans plateformes
    • L17-20 : Récupère chacune des valeurs de cette plateforme
    • L21 : on utilise rect() pour afficher le rectangle qui correspond à cette plateforme.

⚙ 06-C° Isolons mentalement les lignes qui permettent de gérer les personnages.

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
perso1 = {'x':90, 'y':0, 'w':4, 'h':8, 'c':1} # joueur 1 perso2 = {'x':50, 'y':0, 'w':4, 'h':8, 'c':2} # joueur 2 def controler() -> None: """Modifie les données 30 fois par seconde""" gerer_mouvement_perso(perso1, pyxel.KEY_LEFT, pyxel.KEY_RIGHT, pyxel.KEY_UP) gerer_mouvement_perso(perso2, pyxel.KEY_A, pyxel.KEY_Z, pyxel.KEY_SPACE) def gerer_mouvement_perso(perso:dict, g:int, d:int, s: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 :: param s(int) :: un entier correspondant à la touche pour sauter 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, d, s : 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 gauche perso['x'] = perso['x'] - 1 # on déplace à gauche if pyxel.btn(d): # si appuie sur la touche droite perso['x'] = perso['x'] + 1 # on déplace à droite if pyxel.btn(s) and not peut_tomber(perso): # si appuie sur la touche saut en étant sur une plateforme perso['y'] = perso['y'] - 25 # on saute # 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'])
  1. Quels sont (dans l'ordre) les effets des trois touches qu'on doit envoyer lignes 7 et 8 ci-dessous ?
  2. Comment fonctionne le saut ?
  3. Pourquoi imposer que le personnage ne soit pas en train de tomber pour sauter ?

...CORRECTION...

  1. Déplacement à gauche, déplacement à droite et saut.
  2. Si on appuie sur la touche saut alors que le personnage est posé sur une plateforme, il pourra sauter de 25 pixels : pour cela, on modifie la valeur associée à la clé 'y'.
  3. Tester qu'il ne tombe pas revient à tester qu'il est bien posé sur quelque chose et qu'il peut prendre appui.

⚙ 07° Réaliser maintenant gerer_mouvement_plateformes(). Toutes les 90 images (3s puisqu'il y a 30 images par seconde), on doit décider si le plateforme doit se déplacer à gauche, à droite ou rester immobile avec un peu de hasard.

Pour mettre ce résultat en mémoire, il suffit de modifier la case d'indice 4 de la plateforme : celle qui contient 0 initialement.

On déplacera la plateforme une image sur deux. On ne permettra pas à la plateforme de sortir de l'écran : si elle touche le bord de l'écran, on lui imposera de partir dans l'autre sens, on la fait rebondir contre le bord.

Petit complément utile

Comment connaitre le nombre d'images depuis le lancement du jeu ? En utilisant une variable définie automatiquement par le module Pyxel : frame_count, c'est à dire "compteur de frame" qui contient le nombre de mise à jour depuis le lancement du jeu.

1 2
if pyxel.frame_count % 30 == 0: # Une action toutes les secondes (30 images) faire_un_truc()
1 2
if pyxel.frame_count % 60 == 0: # Une action toutes les 2 secondes (60 images) faire_un_truc()
1 2
if pyxel.frame_count % 15 == 0: # Une action toutes les 0.5 seconde (15 images) faire_un_truc()

...CORRECTION...

Attention, il s'agit d'UNE correction : il y a plein de manière faire cela.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
def gerer_mouvement_plateformes() -> None: """Modifie aléatoirement l'abscisse des plateformes""" if pyxel.frame_count % 90 == 0: for plateforme in plateformes: dpc = random.randint(-1, 1) # on tire -1, 0 ou +1 plateforme[4] = dpc # on place ce déplacement en mémoire if pyxel.frame_count % 4 == 0: for plateforme in plateformes: plateforme[0] = plateforme[0]+plateforme[4] # On modifie x if plateforme[0] == 0: # si la plateforme est trop à gauche plateforme[4] = 1 # on force le mouvement à droite if plateforme[0]+plateforme[2] == 128: # si la plateforme est trop à droite plateforme[4] = -1 # on force le mouvement à gauche

⚙ 08° Passons aux choses sérieuses : réalisons un jeu joueur vs ordinateur. L'un des personnages ne sera plus contrôlé par le joueur mais par l'ordinateur.

Actions à réaliser

  1. Lancer ce nouveau programme.
  2. Trouver qui est le robot et qui est le joueur.
  3. Expliquer pourquoi le robot ne bouge pas pour le moment...
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124
import pyxel import random perso1 = {'x':90, 'y':0, 'w':4, 'h':8, 'c':1} # joueur 1 perso2 = {'x':50, 'y':0, 'w':4, 'h':8, 'c':2} # joueur 2 p1 = [30, 35, 40, 4, 0] # [x, y, largeur, hauteur, déplacement] p2 = [60, 55, 30, 4, 0] p3 = [50, 75, 60, 4, 0] p4 = [10, 95, 60, 4, 0] plateformes = [p1, p2, p3, p4] def controler() -> None: """Modifie les données 30 fois par seconde""" gerer_mouvement_perso(perso1, pyxel.KEY_LEFT, pyxel.KEY_RIGHT, pyxel.KEY_UP) gerer_mouvement_robot(perso2, perso1) def afficher() -> None: """Modifie l'affichage 30 fois par seconde""" pyxel.cls(0) afficher_perso(perso1) afficher_perso(perso2) gerer_mouvement_plateformes() afficher_plateformes() def gerer_mouvement_robot(robot:dict, cible:dict) -> None: """Modifie les coordonnées du robot en fonction de celles de la cible""" pass def gerer_mouvement_plateformes() -> None: """Modifie aléatoirement l'abscisse des plateformes""" if pyxel.frame_count % 90 == 0: for plateforme in plateformes: dpc = random.randint(-1, 1) # on tire -1, 0 ou +1 plateforme[4] = dpc # on place ce déplacement en mémoire if pyxel.frame_count % 4 == 0: for plateforme in plateformes: plateforme[0] = plateforme[0] + plateforme[4] # On modifie x if plateforme[0] == 0: # si la plateforme est trop à gauche plateforme[4] = 1 # on force le mouvement à droite if plateforme[0]+plateforme[2] == 128: # si la plateforme est trop à droite plateforme[4] = -1 # on force le mouvement à gauche def gerer_mouvement_perso(perso:dict, g:int, d:int, s:int) -> None: """Gère les mouvements gauche, droite et chute du personnage reçu""" # 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 gauche perso['x'] = perso['x'] - 1 # on déplace à gauche if pyxel.btn(d): # si appuie sur la touche droite perso['x'] = perso['x'] + 1 # on déplace à droite if pyxel.btn(s) and not peut_tomber(perso): # si appuie sur la touche saut en étant sur une plateforme perso['y'] = perso['y'] - 25 # on saute # 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']) 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 couleur = perso['c'] # largeur du personnage pyxel.rect(xp, yp, largeur, hauteur, couleur) def afficher_plateformes() -> bool: """Affiche les plateformes""" for plateforme in plateformes: x = plateforme[0] # Colonne y = plateforme[1] # Ligne w = plateforme[2] # Largeur / width h = plateforme[3] # Hauteur / height pyxel.rect(x, y, w, h, 10) # Programme principal pyxel.init(128, 128, title="Mon premier jeu") # Initialisation de la fenêtre pyxel.run(controler, afficher) # Alternance controler/afficher

...CORRECTION...

Isolons mentalement les parties qui nous intéressent pour ces questions.

1 2 3 4 5 6 7 8 9 10
def controler() -> None: """Modifie les données 30 fois par seconde""" gerer_mouvement_perso(perso1, pyxel.KEY_LEFT, pyxel.KEY_RIGHT, pyxel.KEY_UP) gerer_mouvement_robot(perso2, perso1) def gerer_mouvement_robot(robot:dict, cible:dict) -> None: """Modifie les coordonnées du robot en fonction de celles de la cible""" pass

On voit qu'on envoie le perso1 à la fonction qui déplace le joueur.

La fonction qui déplace le robot reçoit perso2 en premier (c'est le robot). Pourquoi recevoir le dictionnaire perso1 : simplement car elle va avoir besoin de poursuivre quelqu'un !

Le robot ne bouge pas car sa fonction de mouvement est vide, tout simplement.

⚙ 09° Rajouter les instructions permettant au robot de tomber lorsqu'il est dans le vide. De même rajouter de quoi le faire revenir par le haut s'il tombe trop bas.

...CORRECTION...

1 2 3 4 5 6 7 8 9 10
def gerer_mouvement_robot(robot:dict, cible:dict) -> None: """Modifie les coordonnées du robot en fonction de celles de la cible""" # Gestion des chutes if peut_tomber(robot): robot['y'] = robot['y'] + 1 # Gestion des positions maximales if robot['y'] >=128: robot['y'] = 0

⚙ 10° Lorsque le robot ne tombe pas, on veut maintenant qu'il se dirige à gauche si le personnage est à sa gauche, ou à droite si le personnage est à sa droite.

...CORRECTION...

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
def gerer_mouvement_robot(robot:dict, cible:dict) -> None: """Modifie les coordonnées du robot en fonction de celles de la cible""" # Gestion des chutes if peut_tomber(robot): robot['y'] = robot['y'] + 1 # Gestion de la direction gauche - droite if not peut_tomber(robot): if cible['x'] > robot['x']: robot['x'] = robot['x'] + 1 elif cible['x'] < robot['x']: robot['x'] = robot['x'] - 1 # Gestion des positions maximales if robot['y'] >=128: robot['y'] = 0

⚙ 11° Lorsque le robot ne tombe pas et que la cible est au dessus de lui, on veut maintenant qu'il tente de sauter pour rejoindre la plateforme au dessus de lui.

...CORRECTION...

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
def gerer_mouvement_robot(robot:dict, cible:dict) -> None: """Modifie les coordonnées du robot en fonction de celles de la cible""" # Gestion des chutes if peut_tomber(robot): robot['y'] = robot['y'] + 1 # Gestion de la direction gauche - droite if not peut_tomber(robot): if cible['x'] > robot['x']: robot['x'] = robot['x'] + 1 elif cible['x'] < robot['x']: robot['x'] = robot['x'] - 1 if robot['y'] > cible['y']: # y vers le bas : robot en dessous robot['y'] = robot['y'] - 25 # Gestion des positions maximales if robot['y'] >=128: robot['y'] = 0

⚙ 12° Vous avez dû voir que la méthode ne fonctionne vraiment pas parfaitement : toutes les plateformes sont espacées de 20 pixels.

Modifier une dernière fois le programme : le robot ne doit sauter que s'il existe bien une plateforme au dessus de lui. Pour simplifier le travail, nous allons simplement chercher la couleur avec pget(). Voir la fonction peut_tomber() au besoin.

...CORRECTION...

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
def gerer_mouvement_robot(robot:dict, cible:dict) -> None: """Modifie les coordonnées du robot en fonction de celles de la cible""" # Gestion des chutes if peut_tomber(robot): robot['y'] = robot['y'] + 1 # Gestion de la direction gauche - droite if not peut_tomber(robot): if cible['x'] > robot['x']: robot['x'] = robot['x'] + 1 elif cible['x'] < robot['x']: robot['x'] = robot['x'] - 1 if robot['y'] > cible['y']: # y vers le bas : robot en dessous y_plateforme_possible = robot['y'] + robot['h'] - 20 if pyxel.pget(robot['x'], y_plateforme_possible) != 0: robot['y'] = robot['y'] - 25 # Gestion des positions maximales if robot['y'] >=128: robot['y'] = 0

Nous n'irons pas plus loin aujourd'hui pour trouver une solution exacte. Ce que nous venons de faire correspond à une stratégie Gloutonne : une stratégie qui n'est pas nécessairement exacte mais qui devrait normalement mener à une solution correcte la plupart du temps. Ce sera l'objet d'une des leçons d'algorithmique.

3 - Cours : les alias et les copies

(Rappel) 3.1 Variables en Python : l'espace des noms

En Python, une affectation crée une liaison entre un nom de variable et un contenu-mémoire.

une sorte de liaison
A - L'espace des noms
L'espace des noms est une table où chaque ligne permet de relier une variable à une zone-mémoire.

espace des noms type simple

L'espace des noms ci-dessus pourrait être généré par ce programme :

1 2 3
a = 10 b = 20 c = b - a

La fonction native id() permet de récupérer l'adresse / référence / identifiant référencé par une variable.

>>> a = 10 >>> b = 20 >>> id(a) 108 >>> id(b) 524 >>> id(c) 108

Ces exemples font bien référence à ce cas de figure :

espace des noms type simple
B - LES espaceS des noms
Il existe en réalité plusieurs espaces des noms :

  • Un espace global des noms du programme : cette table est permanente, et existe pendant tout le déroulement du programme.
  • Un espace local des noms pour chaque appel de fonction : chacune de ces tables est temporaire, et est détruite après que la fonction ai répondu.

L'intérêt de ce mécanisme complexe est double :

  • il libére les zones mémoires dès qu'on n'a plus besoin des contenus qui y avaient été stockés. Sans cela, la RAM serait très rapidement pleine.
  • les variables ne "contiennent" pas vraiment le contenu, juste l'adresse du contenu. On évite ainsi de dupliquer certains contenus : si 3 variables référencent le même texte, on ne stocke qu'une fois le texte en mémoire.

Exemple avec ce court programme :

1 2 3 4 5
def f(x): return x * 2 a = 10 b = f(a)
espaces des noms

Une fois que f() a répondu, on supprime son espace des noms et les variables qui y étaient indiquées.

C - Utilisation par Python
Lorsque Python doit évaluer une variable dans une fonction :

  1. Il commence par chercher dans l'espace local de la fonction.
  2. S'il n'y trouve pas de variable locale portant ce nom, il cherche dans l'espace global.
  3. S'il n'y trouve aucune variable globale portant ce nom, il déclenche une exception NameError.

Traduit en Python, cela donnerait quelque chose comme ceci :

1 2 3 4 5 6 7 8 9 10 11 12 13
def evaluer_variable(nom): espace_local = locals() # espace local actuel sous forme d'un dico espace_global = globals() # idem mais pour l'espace global if nom in espace_local: # si le nom est bien une clé de l'espace local return espace_local[nom] # on renvoie son contenu mémoire elif nom in espace_global: # sinon si le nom est une clé de l'espace global return espace_global[nom] # on renvoie son contenu mémoire else: # sinon, raise NameError # on lève l'exception NameError
[Optionnel] Deux stratégies de gestion des variables (niveau avancé)

Le mécanisme que nous venons de voir n'est pas universel dans tous les langages de programmation.

Il existe globalement deux écoles pour gérer l'espace des noms d'une variable a qui passerait de "bonjour" à "hello" par exemple.

Regardons comment on peut gérer en mémoire ce programme.

1 2
a = "bonjour" a = "hello"
  1. Première stratégie : lorsqu'on change le "contenu" de a dans le programme, on modifie juste la zone-mémoire référencée par la variable dans l'espace des noms. L'adresse de la variable change donc à chaque fois qu'on réalise une affectation. C'est la stratégie du langage Python.
  2. première stratégie : on change la liaison
  3. Deuxième stratégie : lorsqu'on change le "contenu" de a dans le programme, on modifie la zone-mémoire qui est référencée par la variable dans l'espace des noms. L'adresse de la variable reste donc la même lorsqu'on réalise une nouvelle affectation. C'est la stratégie du langage C.

    première stratégie : on change le contenu de la zone-mémoire
(Rappel) 3.2 TABLEAU STATIQUE : définition

Un tableau est un conteneur formant une collection ordonnée d'éléments ayant tous le même type. On peut donc avoir un tableau d'integers, ou un tableau statique de floats par exemple.

Les cases sont identifiées par un numéro nommé indice (index en anglais).

Un tableau statique est un tableau comportant un nombre de cases fixé à la création.

Voici par exemple un tableau de caractères de 3 cases :

Indice 0 1 2
Elément 'A' 'B' 'C'

3 cases donc des indices valant 0, 1 et 2.

flowchart LR D([Variable]) --> M([conteneur-tableau]) M -- indice 0 --> A([élément 0]) M -- indice 1 --> B([élément 1]) M -- indice 2 --> C([élément 2])

Notez bien que la première case est la case d'indice 0, pas celle d'indice 1. Si un tableau possède 20 cases, les indices disponibles vont de 0 à 19.

3.3 Variable : fonction native id()

La fonction native id() renvoie l'identifiant de la zone-mémoire associée à la variable dans l'espace des noms.

espace des noms type simple
>>> a = 10 >>> id(a) 108 >>> id(10) 108

Votre propre valeur peut être différente bien entendu.

CPython

Thonny utilise une implémentation de Python réalisée en C. La fonction id() renvoie alors réellement l'adresse-mémoire.

3.4 Tableaux : copie et alias

A - Définition : alias
Des alias sont des variables qui pointent exactement vers la même zone-mémoire.

Modifier l'un, modifie l'autre car c'est le même contenu qui porte deux noms différents.

Pour réaliser des alias en Python, il suffit de réaliser une affectation : les deux variables vont pointer vers le même contenu mutable.

>>> t1 = [10, 20, 30] >>> t2 = t1 # t2 et t1 référencent le même contenu-mémoire >>> id(t1) 139936748427648 >>> id(t2) 139936748427648 >>> t1[0] = 1000 # On modifie t1 >>> t1 [1000, 20, 30] >>> t2 # t2 est modifié aussi [1000, 20, 30]
B - Définition : copie
Une copie pointe vers un contenu-mémoire identique à au contenu qu'une autre zone-mémoire. Mais l'adresse mémoire des deux contenus est différent.
Si on modifie le contenu de la copie, on ne modifie pas le contenu initial.

Si on modifie le contenu initial après copie, on ne modifie pas la copie.

Pour réaliser des copies de tableaux contenant des types simples en Python, il suffit de réaliser une création par compréhension.

>>> t1 = [10, 20, 30] >>> t2 = [v for v in t1] # contenu similaire à t1 mais ailleurs >>> id(t1) 139936748427648 >>> id(t2) 139936751349056 >>> t1[0] = 1000 # On modifie t1 >>> t1 [1000, 20, 30] >>> t2 # t2 n'est PAS modifié [10, 20, 30]
C - Visuel de la différence alias et copie
Copie et alias

t1 et t2 sont des alias puisqu'elles référencent la même zone mémoire.

t3 est une copie du contenu précédent : valeurs identiques mais dans une zone mémoire différente.

4 - FAQ

5 -

Activité publiée le 16 09 2020
Dernière modification : 04 07 2024
Auteur : ows. h.