python fonction et tableaux

Identification

Infoforall

24 - Tableaux et Fonctions


Nous avons vu trois structures de données indexées :

  • Deux non-mutables : le string et le tuple
  • Une mutable : le tableau (type list en Python)

Vous allez découvrir la spécificité du transfert d'un type mutable en tant que paramètre de fonction : la fonction peut parvenir à modifier le paramètre !

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

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

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

1 - Problématique

Nous allons voir aujourd'hui comment les fonctions peuvent interagir avec les tableaux, implémentés en Python avec le type natif list, une structure de données mutable.

Rappel du comportement d'une fonction sur un paramètre

LECTURE POSSIBLE : lecture d'une variable envoyée depuis le programme principal et placée dans un paramètre

Une fonction peut lire le paramètre sans problème.

1 2 3
def lecture(entree): """La fonction parvient à LIRE le paramètre""" return entree
>>> a = 12 >>> lecture(a) 12

L'argument a est envoyé dans le paramètre entree. La fonction parvient à lire 12 en lisant le paramètre entree.

MODIFICATION IMPOSSIBLE : modification d'une variable envoyée depuis le programme principal et placée dans un paramètre

Une fonction ne peut pas modifier une variable du programme principal. Si on réalise une affectation dans la fonction, on ne fait que créer une nouvelle variable locale dans l'espace des noms de la fonction.

1 2 3 4
def modification(entree): """La fonction ne parvient pas à MODIFIER l'argument reçu et stocké dans le paramètre entree""" entree = 0 # Affectation sur entree donc création d'une nouvelle variable locale ! return entree
>>> a = 12 >>> modification(a) 0 >>> a 12

On renvoie bien le 0 enregistré dans la fonction mais la variable a n'a pas été modifiée.

La seule manière de tenir compte de la réponse de la fonction serait de faire cela :

>>> a = 12 >>> a = modification(a) >>> a 0

Remarquez bien que cela fonctionne car il y a une affectation dans la console ou le programme. Ce n'est donc pas la fonction qui agit : elle ne fait que transmettre une réponse.

Regardons maintenant le cas des tableaux qu'on envoie en argument lorsqu'on tente d'accéder à leur contenu.

LECTURE POSSIBLE : une fonction peut LIRE une case du tableau qu'elle reçoit en paramètre

Comme auparavant, rien de neuf.

1 2 3
def lecture(tableau, index): """La fonction parvient à LIRE un tableau fourni en paramètre""" return tableau[index]
>>> a = [12, 45, 90] >>> lecture(a, 0) 12

On rappelle qu'on lit la case numéro index du tableau tableau en tapant tableau[index]

Plus étonnant :

MODIFICATION DU CONTENU POSSIBLE : une fonction peut MODIFIER une case d'un tableau qu'elle reçoit en paramètre

C'est un phénomène totalement nouveau : normalement, on ne peut pas modifier ce qui est contenu dans le programme principal depuis une fonction. Mais avec les tableaux mutables, on va pouvoir modifier le CONTENU du tableau, et pas la variable-tableau elle-même.

1 2 3 4 5 6 7 8 9
def modification(entree, index): """La fonction parvient à MODIFIER le tableau fourni en paramètre""" entree[index] = entree[index] + 10 print(f"Dans la fonction, entree (de la fonction) fait référence à {entree}") entree = [0, 2, 4, 6] print(f"Avant la fonction, entree (du programme) fait référence à {entree}") modification(entree, 0) print(f"Après la fonction, entree (du programme) fait référence à {entree}")

LIGNE 3 : remarquez bien qu'on ne fait pas d'affectation sur tableau mais sur l'une des cases du tableau : entree[index] =.

Le résultat :

Avant la fonction, entree (du programme) fait référence à [0, 2, 4, 6] Dans la fonction, entree (de la fonction) fait référence à [10, 2, 4, 6] Après la fonction, entree (du programme) fait référence à [10, 2, 4, 6]

On voit donc bien qu'à la fin, le contenu de la case d'index 0 du tableau a été modifié.

Nous allons voir pourquoi aujourd'hui.

01° Créer une fonction qui attend un paramètre contenant un tableau et qui modifie toutes ses cases en les multipliant par deux. Comme on doit MODIFIER les cases du tableau, vous utilisez une boucle FOR numérique.

Comme vous avez besoin de connaître le numéro d'index de l'élément pour le modifier, il faut utiliser
for index in range(len(tableau)) avec

  • un accès au contenu de la case avec tableau[index]
  • une modification du contenu de la case avec tableau[index] = nouveau_contenu

Attention : dans l'exemple la variable du tableau se nomme tableau et dans la fonction, le paramètre est nommé entree.

1
def fois2(entree:list) -> None:

Exemple d'utilisation :

>>> a = [1,10,100] >>> fois2(a) >>> a [2, 20, 200]

...CORRECTION...

1 2 3
def fois2(entree): for index in range ( len(entree) ): entree[index] = entree[index] * 2

02° La fonction précédente renvoie-t-elle quelque chose qui pourrait justifier la modification de contenu ?

...CORRECTION...

Non, la fonction ne renvoie rien. Le contenu du tableau est bien modifié directement depuis la fonction.

03° Analyser la fonction ci-dessous. Fait-elle la même chose que la fonction précédente ? Pour répondre à cela, demander vous comment l'activer correctement en choisissant entre les deux commandes suivantes :

Commande A :

>>> a = [1,10,100] >>> fois2(a)

Commande B :

>>> a = [1,10,100] >>> b = fois2(a)
1 2 3 4 5
def fois2(entree): copie = [valeur for valeur in entree] for index in range ( len(copie) ): copie[index] = copie[index] * 2 return copie

...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 entrée n'est pas modifié.

La commande B est donc indispensable si on veut garder la nouvelle version en mémoire :

>>> a = [1,10,100] >>> b = fois2(a)

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

1 2 3
def fois2(entree): copie = [valeur*2 for valeur in entree] return copie

Deux questions

  1. Un utilisateur pourrait-il voir que les deux fonctions n'ont pas implémenté l'action de la même façon sur les questions 3 et 4 ?
  2. Comment se nomme la façon de créer la copie ligne 2 ?

...CORRECTION...

Les deux fonctions des questions 3 et 4 renvoient la même sortie. Elles sont juste codées différemment. L'implémentation interne est différente, le résultat est identique.

La méthode de création se nomme création par compréhension.

Imaginons que nous ayons à gérer un jeu où le personnage est défini par trois jauges.

  • Le nombre de points de vie est stocké à l'index 0.
  • Le nombre de points de magie est stocké à l'index 1.
  • Le nombre de points de chance est stocké à l'index 2.

jauges = [50, 60, 80] veut dire

  • 50 points de vie (pv)
  • 60 points de magie
  • 80 points de chance

✎ 05° Que devrait renvoyer l'évaluation de jauges[1] ?

Pour rappel : jauges = [50, 60, 80].

Nous allons déclarer une fonction diminuer_pv, fonction dont voici le prototype :

1
def diminuer_pv(jauges:list, degats:int) -> bool:

On doit lui fournir 2 paramètres dans cet ordre :

  • le tableau jauges à modifier,
  • la valeur des degats à soustraire à l'index 0 (les points de vie)

La fonction doit renvoyer True si les points de vie sont toujours positifs après diminution. Et donc False dans le cas contraire.

Exemple d'utilisation :

>>> heros = [50, 60, 80] >>> diminuer_pv(heros, 40) True >>> heros [10, 60, 80]

On a bien diminué la jauge d'index 0 de 40 points : on passe de 50 initialement à 10 après diminution. Comme il en reste 10, la fonction répond True.

06° Que vont contenir les paramètres jauges et degats sur cet appel ?

>>> heros = [50, 60, 80] >>> diminuer_pv(hero, 20)

Pour rappel, voici le prototype de la fonction :

1
def diminuer_pv(jauges:list, degats:int) -> bool:

...CORRECTION...

Il faut regarder l'appel et la déclaration de la fonction. Il suffit alors de travailler par identification.

1
def diminuer_pv(jauges:list, degats:int) -> bool:
>>> diminuer_pv(heros, 20)
  1. Le paramètre jauges va donc recevoir la référence du tableau heros
  2. Le paramètre degats va donc recevoir l'integer 20

07° Expliquer clairement quelle jauge doit diminuer, de combien et le contenu attendu dans le tableau heros après modification.

>>> heros = [50, 60, 80] >>> diminuer_pv(hero, 20)

...CORRECTION...

On veut modifier la jaude d'index 0, celle qui vaut 50 au début.

On veut les diminuer de 20 points.

Le personnage aura donc 30 points de vie après modification.

Une lecture du contenu donnerait ceci :

>>> heros [30, 60, 80]

✎ 08° Cas théorique : que va afficher la console Python lorsqu'on lui demande de fournir le contenu de la variable lePersonnage ?

>>> lePersonnage = [80, 40, 30] >>> diminuer_pv(lePersonnage, 30) True >>> lePersonnage

09° Cas théorique : que va afficher la console Python lorsqu'on lui demande de fournir le contenu de la variable lePersonnage ?

>>> lePersonnage = [80, 40, 30] >>> diminuer_pv(lePersonnage, 90) ???

...CORRECTION...

La fonction va renvoyer False puisque la jauge des points de vie est devenue nulle : 80 - 90 = -10.

10° La fonction diminuer_pv fournie ci-dessous ne fait rien de particulier pour le moment : elle renvoie toujours True...

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
def diminuer_pv(jauges, degats): """Fonction qui modifie les pv du personnage en lui enlevant degats et renvoie -> True si les pv sont > 0 après réduction -> False sinon :: param jauges(list) :: le tableau contenant les jauges du personnage :: param degats(int) :: le nombre de dégâts à enlever :: return (bool) :: True si perso actif / False si HS >>> diminuer_pv([50,20,10], 50) False >>> diminuer_pv([50,20,10], 60) False >>> diminuer_pv([50,20,10], 40) True """ return True perso_1 = [50, 60, 80] print(perso_1) disponible = diminuer_pv(perso_1, 40) if not disponible: print("Personnage indisponible pour le moment") else: print("Personnage ok pour le moment.") print(perso_1)

Puisque les jauges du personnage sont stockées dans le paramètre jauges, on peut

  • Lire les PV avec jauges[0]
  • Modifier les PV avec jauges[0] = 45 par exemple.

Question : remplacer maintenant la ligne 19 par plusieurs autres lignes de façon à réaliser ceci :

    SI les pv sont > 0

      On diminue la jauge des pv de degats

    Fin SI

    Renvoyer pv > 0

En modifiant les dégats envoyés lors de l'appel test de la ligne 23, vos modifications devront permettre de renvoyer False si les points de vie deviennent nuls ou négatifs.

...CORRECTION...

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
def diminuer_pv(jauges, degats): """Fonction qui modifie les pv du personnage en lui enlevant degats et renvoie -> True si les pv sont > 0 après réduction -> False sinon :: param jauges(list) :: le tableau contenant les jauges du personnage :: param degats(int) :: le nombre de dégâts à enlever :: return (bool) :: True si perso actif / False si HS >>> diminuer_pv([50,20,10], 50) False >>> diminuer_pv([50,20,10], 60) False >>> diminuer_pv([50,20,10], 40) True """ if jauges[0] > 0: jauges[0] = jauges[0] - degats return jauges[0] > 0

✎ 11° Expliquer, ligne par ligne, ce que fait le programme sur ces lignes. Fournir le résultat affiché si on demande à la console d'afficher quelque chose.

32 33 34 35 36 37 38 39
perso_1 = [50, 60, 80] print(perso_1) disponible = diminuer_une_jauge(perso_1, 70) if disponible == False: print("Personnage indisponible pour le moment") else: print("Personnage ok pour le moment.") print(perso_1)

Vous devriez être parvenu à voir que cela fonctionne. Mais...

Le tableau contenant les jauges du personnage se nomme perso_1 et le paramètre dans la fonction jauges. Comment se fait-il qu'une modification sur jauges_de_la_fonction provoque une modification de perso_1_du_corps_du_programme ?

L'explication dans la partie suivante.

2 - Effet de bord

12° Lancer le programme ci-dessous puis répondre à ces questions :

  • A quel identifiant-mémoire fait référence la variable tableau ?
  • Dans quel paramètre stocke-t-on l'argument tableau lorsqu'on l'envoie ?
  • A quel identifiant-mémoire fait référence le paramètre entree ?
  • La variable entree du programme principal et la variable entreede la fonction désignent-elles la même zone mémoire ?
    • 1 2 3 4 5 6 7 8
      def nouveau(entree): print(f"Id du paramètre entree : {id(entree)}") input("Appuyer sur ENTREE pour sortir de la fonction") tableau = [10, 20, 30] print(f"Id de la variable tableau : {id(tableau)}") input("Appuyer sur ENTREE pour entrer dans la fonction") nouveau(tableau)

      Remarque : la fonction native input ne sert ici qu'à provoquer une pause dans le déroulement du programme.

...CORRECTION...

Voici un affichage possible :

Id de la variable tableau : 140225629356232 Appuyer sur ENTREE pour entrer dans la fonction Id du paramètre entree : 140225629356232 Appuyer sur ENTREE pour sortir de la fonction

On voit bien que les deux variables désignent donc la même zone mémoire.

C'est normal : nous avons transmis le tableau en le placant dans le paramètre de la fonction.

Fonction et paramètres mutables : l'effet de bord

En informatique, une fonction est dite à effet de bord si elle modifie l'état d'une structure de données non locale à la fonction elle-même. C'est le cas de la fonction diminuer_pv : elle va modifier l'état du tableau qu'on a fourni.

L'appel envoie la référence-mémoire du tableau perso_1 : diminuer_pv(perso_1, 70). Cela veut dire que les deux variables-tableaux désignent la zone-mémoire 12 par exemple.

Voici ce qu'on obtient sur Python Tutor lors de l'appel de la fonction :

effet de bord
Les deux variables-tableaux désignent la même zone mémoire

C'est assez visuel : les deux variables pointent bien vers la même chose. Les deux noms sont en réalite des alias qui désignent la même chose.

Du coup, si je modifie l'index 0 de jauges, une fois de retour dans le programme principal, je constaterai bien que l'index 0 de perso_1 a changé puisque les deux variables désigent en réalité la même chose !

Documentation : les effets de bords sont à signaler dans la documentation de vos fonctions. Donc, si votre fonction modifie un tableau reçu en paramètre, il faut le signaler dans la documentation.

Une courte explication : le fait qu'en Python, les variables fassent référence à des identifiants mémoires provoquent des effets dont il faut tenir compte.

>>> tableau = [4, 40, 400] >>> autre = tableau

Aucune des deux variables ne contient vraiment [4, 40, 400]. En réalité, elles contiennent l'identifiant-mémoire de la zone qui contient [4, 40, 400].

>>> id(tableau) 384 >>> id(autre) 384

La conséquence ? Il ne s'agit pas de deux contenus différents mais d'un seul contenu qui possède plusieurs alias. Si on modifie l'un, on modifie l'autre.

>>> autre[0] = 8000 >>> autre [8000, 40, 400] >>> tableau [8000, 40, 400]

C'est le même phénomène avec un tableau envoyé en tant qu'argument à une fonction : le paramètre de réception ne contient pas le contenu du tableau mais sa référence-mémoire. Du coup, modifier le paramètre modifie aussi le tableau d'origine !

13° Vérifier ce que nous venons de dire en utilisant ce code :

1 2 3 4 5 6 7 8 9
def modifier(tableau): tableau[0] = 'Ca' tableau[1] = 'Alors' tableau[2] = '!' message = ['Ceci', 'au', 'départ'] print(message) modifier(message) print(message)

Nous allons donc visualiser que le tableau message contient ceci au début :

['Ceci', 'au', 'départ']

Et ceci après action de la fonction, qui agit pourtant uniquement sur le paramètre tableau, une variable locale à la fonction !

['Ca', 'Alors', '!']

14° Observer le code suivant sur Python Tutor. Donner le contenu du tableau notes après exécution du programme.

1 2 3 4 5 6 7 8
def modifier(a, b): a[0] = 0 b[0] = 20 notes = [12, 15, 8] coeffs = [5, 4, 3] modifier(coeffs, notes)

...CORRECTION...

Lors de l'appel, on envoie le tableau notes en deuxième position.

On stocke donc sa référence dans le deuxième paramètre de la fonction, soit b.

Python Tutor montre bien comment les arguments sont stockés dans les paramètres

On va donc modifier l'index 0 de b et cela va modifier l'index 0 de notes puisque les deux variables désignent la même adresse-mémoire.

Le tableau notes va donc contenir [20, 15, 8] à la fin.

Python Tutor montre bien comment le paramètre permet de modifier l'état du tableau

3 - Bilan

✎ 15° Expliquer le plus clairement possible ce que fait cette fonction sur le tableau qu'on lui passe en argument.

1 2 3
def mystere(tableau): for index in range( len(tableau) ): tableau[index] = 10

✎ 16° Créer une fonction-procédure multiplication qui modifie le tableau par effet de bord en multipliant les éléments du tableau transmis par le second paramètre coef.

Dans votre fonction, il faudra donc lire les index possibles à l'aide d'une boucle FOR numérique (munie d'un range) et modifier les cases une par une à l'aide du nom du tableau dans la fonction et de l'index de travail.

Prototype de la fonction : def multiplication(tableau:list, coef:int) -> None:.

Exemple d'utilisation :

>>> valeurs = [1, 2, 3, 4] >>> multiplication(valeurs, 10) >>> valeurs [10, 20, 30, 40]

4 - FAQ

Ca à l'air magique l'effet de bord, non ? Ca fonctionne comment ?

L'appel envoie la référence-mémoire du tableau perso_1 : diminuer_pv(perso_1, 70). Cela veut dire que les deux variables-tableaux désignent la zone-mémoire 12 par exemple.

effet de bord
Les deux variables-tableaux désignent la même zone mémoire

Lorsqu'on tape perso_1[0], on va donc voir l'index 0 situé à l'adresse &12.

Lorsqu'on tape jauges[0], on va donc voir l'index 0 situé à l'adresse &12.

Dans les deux cas, on lit l'adresse &100 qui désigne 50.

Que se passe-t-il alors lorsque la fonction diminue l'index 0 du tableau jauges avec l'instruction suivante ?

jauges[0] = jauges[0] - degats

On commence par la droite : l'ordinateur récupère donc le 50 et calcule 50-70 ce qui donne -20.

jauges[0] = -20

La variable jauges n'est pas modifiée : elle désigne toujours &12. On va juste aller à l'adresse &12 et modifier la référence mémoire liée à l'index 0 : on place l'adresse &220 qui fait référence à -20 !

effet de bord
Visualisation de l'effet de bord

Une fois de retour dans le programme principal, que vaut-on obtenir si on veut voir le contenu perso_1[0] ?

On part donc voir l'adresse &12 et on cherche le contenu associé à l'index 0 : la référence-mémoire &220. Et qu'y trouve-t-on ? -20.

Rien de magique en réalité !

La prochaine activité fera le point sur les différentes techniques à utiliser sur les tableaux en fonction des choses qu'on veut faire sur eux.

Comment faire si on veut juste lire un tableau transmis à une fonction ?

Comment faire si on veut modifier un tableau transmis à une fonction ?

Comment faire si on veut créer une copie modifiée ou pas d'un tableau et la faire renvoyer par une fonction ?

Les deux points importants vus aujourd'hui :

  1. Une fonction peut modifier le contenu d'un tableau si elle reçoit ce tableau en paramètre : elle utilse pour cela une affectation sur le contenu à l'aide des crochets. Exemple 
  2. tableau[index] = 200

  3. Si on veut agir sur toutes les cases, une seule manière de boucler : il faut impérativement utiliser une boucle énumérant les index disponibles. Exemple :
  4. 1 2 3 4 5 6
    def modifier(tableau): for index in range( len(tableau) ): tableau[index] = tableau[index] ** 2 notes = [15, 18, 8, 10, 12, 15, 20, 5, 12, 17, 12, 10, 18, 4] modifier(notes)

Activité publiée le 16 09 2020
Dernière modification : 03 12 2020
Auteur : ows. h.