Favicon
NSI Première

Connexion élèves

Choisir le(s) module(s) à installer :

Notions avancées sur les fonctions

Portée des variables

Un exemple

Voila quelques essais d'un élève pour écrire une fonction qui modifie le score d'un joueur dans un jeu :

def augmenter_score(score): score = score + 3 return score score = 5 print(augmenter_score(score)) print(score)

Comment interpréter le résultat affiché ?

  1. l'instruction print à la ligne 6 affiche le résultat renvoyé par la fonction, en lui ayant passé la valeur de score comme argument; pas de problème...
  2. l'instruction print à la ligne 7 affiche la valeur de score telle qu'elle était avant l'appel de la fonction : score n'a donc pas du tout été modifiée lors de cet appel....

Tout se passe comme si il y avait en fait deux variables score différentes dans ce code, l'une dans la fonction, et l'autre en dehors...

Bon...je n'ai qu'à essayer d'affecter directement à la variable score la valeur de celle dans la fonction, en lui donnant un autre nom :

def augmenter_score(sc): sc = sc + 3 return sc score = 5 print(augmenter_score(score)) score = sc

Ah, maintenant j'ai une erreur...on dirait que je ne peux pas "accéder" à la variable sc depuis "l'extérieur" de la fonction....

Je sais ! Je vais directement modifier la variable score dans la fonction, sans la passer en argument :

def augmenter_score(): score = score + 3 return score score = 5 print(augmenter_score()) print(score)

Non, ça ne fonctionne toujours pas...comment interpréter tout cela ??

Variables locales et globales

Selon l'endroit du code où elles ont été définies ( = initialisées ), les variables n'ont pas la même portée, c'est à dire la (ou les) partie(s) du code où elles sont effectivement accessibles( = utilisables ) :

  • Les variables définies dans une fonction sont appelées variables locales. Elles ne peuvent être utilisées que localement c’est-à-dire qu’à l’intérieur de la fonction qui les a définies.

    En fin de fonction, ces variables sont "effacées" par Python et n'existent plus en dehors de la fonction : tenter d’appeler une variable locale depuis l’extérieur de la fonction qui l’a définie provoquera donc une erreur.

    Les paramètres d'une fonction sont par définition des variables locales ( comme le paramètre score dans le premier exemple ci-dessus ).

  • Les variables définies ( = initialisées ) dans le programme principal du script, c’est-à-dire en dehors de toute fonction, sont appelées des variables globales.

On peut légitiment penser que la portée d'une variable globale est le script tout entier, fonctions y comprises; c'est le cas, mais à moitié seulement :

def multiplier_score(score): return score*b def ajouter_score(score): b = b + 3 return score*b score = 5 b = 3 print(multiplier_score(score)) print(ajouter_score(score))
  1. l'appel de la fonction multiplier_score ( ligne 10 ) ne pose pas de problème : la fonction renvoie bien le résultat de l'expression param*b, ce qui signifie qu'elle a pu "lire" sans problème le contenu de la variable b, variable globale.
  2. l'appel de la fonction ajouter_score ( ligne 11 ) provoque par contre une erreur, à la ligne 5, c'est à dire où on a essayé de modifier la variable globaleb.

Les variables globales sont donc accessibles (= utilisables) à travers l’ensemble du script, mais en lecture seulement à l’intérieur des fonctions utilisées dans ce script.

Une fonction va pouvoir utiliser la valeur d’une variable définie globalement mais ne va pas pouvoir modifier sa valeur, c’est-à-dire la redéfinir; si on essaie de redéfinir une variable globale à l’intérieur d’une fonction, on ne fera que créer une autre variable de même nom que la variable globale qu’on souhaite redéfinir mais qui sera locale et bien distincte de cette dernière ( comme la variable score dans le premier exemple ci-dessus ).

Conséquences

Modifier une variable globale depuis une fonction

Et alors, comment résoudre notre problème : pouvoir modifier dans une fonction la valeur du score du joueur ??

Une solution serait d'utiliser le mot-clé global dans la fonction, qui lui indique que ce que l'on veut modifier est bien la variable globale score, et pas une hypothétique variable locale qui n'existe en fait pas :

def augmente_score(): global score score += 3 score = 5 augmente_score() print(score)

Et là, ça marche ! Cependant, notez bien que l'on conseille d'éviter au maximum ce genre d'usage des variables globales, c'est un véritable nid à problèmes très difficiles à résoudre...

Méthode préférable : on peut très bien s'en passer, en passant la variable globale en argument, et en récupérant la valeur modifiée de la variable comme résultat, que l'on réaffecte alors dans le programme principal à la variable globale :

def augmente_score(score): score = score + 3 # 'score' est ici bien sûr une variable LOCALE ! return score score = 5 score = augmente_score(score) print(score)

En résumé...

  • une variable définie dans une fonction est appelée variable locale.
  • une variable définie dans le programme principale est appelée variable globale.
  • la portée d'une variable locale est la fonction où elle a été définie.
  • la portée d'une variable globale est le script tout entier, mais uniquement en lecture seule au sein des fonctions.
  • si on veut ( vraiment... ) modifier une variable globale dans une fonction, on la passe comme paramètre, et on lui réaffecte le résultat renvoyé par la fonction.

Application

Pour finir, vous allez essayer de réutiliser cette notions de portée de variable sur un exemple plus complexe.

Vous trouverez ci-dessous un extrait d'un code simulant le jeu de la "petite sœur" vu en projet.
Pour simplifier votre travail, ce code est limité : il ne gère pas le comptage de points, mais uniquement l'affichage des cartes à chaque fois qu'un joueur choisit entre "gauche" et "droite".

Vous devez corriger le code ci-dessous afin qu'il s'exécute correctement, en pensant bien aux conseils donnés ci-dessus ( pas de variable globale, nom des variables différents dans les fonctions et dans le programme principal,...)

Attention, l'affichage dans l'éditeur ci-dessous ne se fait qu'à la fin de l'exécution du script ( ce n'est pas une erreur de votre part, ça marche comme ça, désolé....😕 ).

Si vous voulez visualiser le déroulement du jeu, exécutez plutôt ce script sous Pyzo.

##############IMPORTS######################### from random import shuffle #################FONCTIONS################### def tire_n_cartes(jeu, N): shuffle(jeu) return [jeu[i] for i in range(N)] def affiche_n_cartes(jeu): for i in range (gauche, droite): print(jeu[i], end = '') def joue(cote): if cote == 'g': gauche = gauche + 1 affiche_n_cartes(jeu_tiré) if cote == 'd': droite = droite - 1 affiche_n_cartes(jeu_tiré) ##########PROGRAMME PRINCIPAL################ jeu_cartes = [('7','Carreau'), ('8','Carreau'),('9','Carreau'), ('10','Carreau'),('V','Carreau'),('D','Carreau'),('R','Carreau'),('A','Carreau'),('7','Coeur'),('8','Coeur'),('9','Coeur'), ('10','Coeur'),('V','Coeur'),('D','Coeur'),('R','Coeur'),('A','Coeur'),('7','Pique'),('8','Pique'),('9','Pique'), ('10','Pique'),('V','Pique'),('D','Pique'),('R','Pique'),('A','Pique'),('7','Trèfle'),('8','Trèfle'),('9','Trèfle'), ('10','Trèfle'),('V','Trèfle'),('D','Trèfle'),('R','Trèfle'),('A','Trèfle')] jeu_tire = tire_n_cartes(jeu_cartes, 10) gauche = 0 droite = len(jeu_tire) while gauche != droite: print("\n") cote = input('g ou d ?') joue(cote)

Lien vers les RÉPONSES

Portée des tableaux

Tout ce qui vient d'être dit est valable pour les types de variables immuables ( = non-mutable ), comme int, float, str, tuple,...

Vous allez voir maintenant qu'avec les types muables ( = mutable ), c'est un peu différent, et cela peut entraîner quelques problèmes dont il faut bien avoir conscience...

Rappel : muable/immuable

Un type est non mutable ( = immuable ) si la valeur d'une variable de ce type ne peut changer que par l'affectation d'une nouvelle valeur à cette variable.
Dans le cas contraire, il sera mutable; on peut modifier un de ses élément sans avoir à le réaffecter complètement.

  • types non mutables : entiers, flottants, booléens, chaînes de caractères, tuples
  • types mutables : tableaux, dictionnaires

Tableau en paramètre de fonction

Une fonction peut sans problème prendre comme paramètre un tableau ; à l'appel de la fonction, il faudra donc lui passer un tableau comme argument.

Reprenons nos exemples de cette page, mais le score à modifier est cette fois un tableau de scores :

def augmenter_score(score): score.append(3) return score score = [5, 6] print(augmenter_score(score)) print(score)

Ah alors là, je comprends plus, le tableau a été modifié dans la fonction, et cette modification "perdure" dans le programme principal...

Ben, facile de résoudre le problème : il suffit de donner un autre nom au paramètre :

def augmenter_score(sc): sc.append(3) return sc score = [5, 6] print(augmenter_score(score)) print(score)

→ et non, toujours la même chose : contrairement aux variables immuables des exemples précédents, score et sc ne semblent désigner ici qu'un seul et même tableau !

Et pire, si j'écris :

def augmenter_score(): score.append(3) score = [5, 6] augmenter_score() print(score)

Même sans le passer comme paramètre, le tableau score, modifié dans la fonction, reste modifié dans le programme principal !!

Conclusion

Quelque soit le nom utilisé pour le désigner, il n'y a donc qu'un unique objet correspondant au tableau dans tout le script, et ce, toujours à cause du caractère mutable d'un tableau.

Les deux tableaux ne semblent donc pas être indépendants, et en réalité, c'est exactement ce qu'il se passe...pour mieux comprendre, il faut se plonger un peu dans la manière dont Python gère la mémoire : cette page explique notamment très bien en quoi le modèle de la "boîte dans laquelle on met une donnée" pour les variables est en fait complètement faux en Python ( elle est vraie dans d'autres langages plus bas niveau, comme le C par exemple ).

Pour visualiser ceci, on peut utiliser le module Python Tutor ( après l'avoir installé dans l'éditeur ci-dessous ) :

from tutor import tutor def augmenter_score(sc): sc.append(3) return sc score = [5, 6] print(augmenter_score(score)) print(score) tutor() # afficher la sortie graphique de l'éditeur pour visualiser l'exécution du module

En résumé, une variable en Python représente en réalité une "étiquette" VERS un emplacement mémoire stockant une donnée; quand on affecte une variable à une autre, on ne crée pas un nouvel emplacement mémoire, mais une nouvelle étiquette, un "alias" de la première, les deux pointant donc exactement vers le même emplacement mémoire.
L'avantage étant qu'on encombre moins la mémoire centrale puisque les données stockées ne figurent qu'en un seul emplacement mémoire.

Dans le cas des types de variable mutables comme les tableaux, modifier une des variables reviendra donc à modifier également l'autre.
Par contre, ce problème n'apparaît pas avec les types non mutables comme les entiers ou les chaînes de caractères, pour lesquels Python crée bien un nouvel emplacement mémoire dès qu'il "détecte" que l'on veut modifier une copie d'une variable.

Les types mutables ont toujours une portée globale ( en lecture et en écriture ) à tout le script.

A titre de documentation, une page pour aller plus loin avec ces histoires de mutable/non mutable...

Copie d'un tableau

Le problème dans le cas du script ci-dessus : le tableau a été complètement modifié par la fonction, et on a donc perdu les données initiales. On parle d'effet de bord de la fonction, au sens "d'effet secondaire" ou "effet indésirable".

Cela ne se serait bien entendu pas produit si l'on avait au préalable pris soin, au début de la fonction, de faire une copie du tableau passé en paramètre, et en ne modifiant que cette copie.

→ on résout ainsi le problème en créant une nouvelle variable, locale à la fonction, et donc bien distincte de celle du programme principal.

Mauvaise façon de copier....

Copier deux variables, on sait faire : il suffit d'utiliser l'opérateur d'affectation = :

def augmenter_score(score): sc = score # je tente une copie du tableau pour ne pas le modifier sc.append(3) # et bien entendu, je modifie la copie... return sc score = [5, 6] print(augmenter_score(score)) print(score)

Que constatez-vous ? D'après ce qui a été dit juste avant, est-ce normal ?

Copie "légère"

Il faut en réalité copier élément par élément un tableau dans une nouvelle variable pour en réaliser une "vraie" copie, version "indépendante" de la première :

def augmenter_score(score): sc = [elt for elt in score] # je fais une VRAIE copie du tableau sc.append(3) return sc score = [5, 6] print(augmenter_score(score)) print(score) # et là, 'score' n'a pas été modifié...

Plusieurs syntaxes sont possibles pour faire cette copie :


b = [element for element in a]
b = [a[i] for i in range(len(a))]
b = a[:]  # encore plus simple !			
			

Python fait une copie dite "légère" des éléments du tableau, mais c'est toujours l'adresse mémoire des objets qui est copiée. Aussi, si l'un des éléments du tableau est aussi un tableau ( nous verrons cela au chapitre suivant ), on retombe sur le même problème.

Copie "profonde"

Pour contourner totalement le problème, il faut utiliser la méthode tableau.deepcopy() du module copy qui effectue une copie dite "profonde":


from copy import deepcopy
.....

b = deepcopy(a) # copie "profonde"
			

D'où cette règle primordiale :

Si dans une fonction on veut modifier les données d'un objet mutable ( comme un tableau ) mais en conserver les données d'origine, on prendra soin d'en faire au préalable une copie et de ne modifier que cette copie.

Attention également au fait que l'on peut donc modifier un tableau dans une fonction, même si on ne l'a pas passé comme argument à cette fonction !!

Applications

Écrêtage

Dans cet exercice, on cherche à modifier les données stockées dans un tableau, mais sans modifier le tableau d'origine.

On dit qu'on écrête un signal lorsqu'on limite l'amplitude du signal entre deux valeurs a et b.
On peut également appliquer cela à des tableaux de valeurs. Voici par exemple un tableau tab que l'on a écrêté entre - 150 et 150 pour donner le tableau tab_ec :


tab =    [34, 56, 89, 134, 152, 250, 87, -34, -187, -310]
tab_ecrete = [34, 56, 89, 134, 150, 150, 87, -34, -150, -150]
			

Les valeurs dans le tableau sont donc maintenant toutes comprises entre -150 et 150.

  1. Compléter la fonction ecrete() ci-dessous qui prend en paramètres un tableau d'entiers tab ainsi que deux entiers a et b avec a <= b, et qui renvoie un nouveau tableau correspondant aux valeurs de tab écrêtées entre a et b.
  2. Tester la fonction avec le tableau ci-dessous et les deux valeurs pour a et b proposées, et vérifier son "bon" fonctionnement.
  3. Vérifier que les données d'origine sont bien toujours "disponibles".
def ecrete(tab, a, b): pass return tab_ecrete t = [35, 47, 47, 34, 82, 81, 1, 63, 60, 40, 83, 29, 66, 97, 54, 21, 73, 40, 3, 86, 25, 10, 59, 56, 72, 97, 62, 45, 54, 13, 30, 68, 12, 17, 68, 3, 54, 71, 85, 23, 45, 4, 9, 21, 45, 84, 62, 16, 3, 34] a = 20 b = 80

Lien vers les RÉPONSES

Permutations pour mélanger

Un algorithme, explicité par Donald Knuth, de mélange uniforme d'un tableau tab de taille N est le suivant :


Pour chacun des éléments du tableau :
		permuter l'élément courant avec un des éléments - choisi au hasard - d'indice inférieur ou égal à celui de l'élément courant
			

En programmation informatique, une permutation consiste à intervertir les valeurs de deux variables. Il s'agit d'une opération courante, mais rarement intégrée aux langages de programmation et jeu d'instructions des processeurs.
De nombreux algorithmes, en particulier des algorithmes de tri, utilisent des permutations.

Pour un tableau on peut facilement permuter deux valeurs en utilisant la double affectation ( qui utilise sans le montrer des tuples ).
Par exemple pour permuter les valeurs 999999999 et 333 d'indices 2 et 8 dans le tableau suivant :


tab = [1, 22, 999999999, 4444, 55555, 666666, 7777777, 88888888, 333]
			

C'est à dire effectuer :

              _______________________________________________
             |                                               |
             V                                               |
 [1, 22, 999999999, 4444, 55555, 666666, 7777777, 88888888, 333]
             |                                               Λ
             |_______________________________________________|
			

Il suffit d'écrire l'instruction suivante :


tab[2], tab[8] = tab[8], tab[2]
			

Il est primordial de noter qu'une permutation ainsi effectuée est une mutation du tableau qui ne nécessite pas de créer un nouveau tableau.
En conséquence, sur un tableau passé en paramètre à une fonction, une mutation effectuée dans le corps de la fonction mutera le tableau à l'extérieur de la fonction.

  1. Compléter la fonction melanger() ci-dessous qui prend en paramètre un tableau tab de longueur quelconque et le mute afin de mélanger uniformément toutes ses valeurs selon l'algorithme proposé ci-dessus.
  2. Tester la fonction avec le tableau proposé en affichant le résultat renvoyé par la fonction.
  3. Supprimer l'instruction return; afficher le tableau, exécuter la fonction et afficher à nouveau le tableau : celui-ci a-t-il été modifié ? Si oui, comment peut-on l'expliquer ?
from random import randint def melanger(tab): pass t = [35, 47, 47, 34, 82, 81, 1, 63, 60, 40, 83, 29, 66, 97, 54, 21, 73, 40, 3, 86, 25, 10, 59, 56, 72, 97, 62, 45, 54, 13, 30, 68, 12, 17, 68, 3, 54, 71, 85, 23, 45, 4, 9, 21, 45, 84, 62, 16, 3, 34]

Lien vers les RÉPONSES

Trier un tableau de 0 et de 1

Il existe de très nombreux algorithmes de tris en informatique, vous verrez bientôt les deux qui sont au programme.

Ces algorithmes ne sont pas très efficaces ( ils prennent du temps...), mais nous allons ici nous intéresser à une situation particulière : un tableau constitué de 2 valeurs différentes seulement, 0 ou 1 : par exemple : [1, 0, 1, 1, 1, 0, 1].

Le but est donc de trier ce tableau, en gardant le même nombre d'éléments, de façon à ce que tous les 0 soient au début, suivis de tous les 1 : [0, 0, 1, 1, 1, 1, 1].

Un algorithme efficace pour faire cela existe :

  1. compléter la fonction tri_01 ci-dessous, qui prend en paramètre un tableau t constitué uniquement de 0 ou de 1, et qui le mute pour le trier selon l'algorithme précédent.
  2. écrire une fonction tableau_01 qui permet de créer aléatoirement un tableau ne contenant que des 0 ou des 1, de longueur N quelconque, passée en paramètre.
    Cette fonction sera écrite en une seule ligne, en utilisant une construction par compréhension judicieusement écrite.
  3. à l'aide de ces deux fonctions, tester le bon fonctionnement de l'algorithme, en affichant le tableau non trié, puis trié

on rappelle l'existence de la fonction randint qui permet de tirer au hasard un nombre entre deux bornes incluses :


from random import randint  # en début de script
	
alea = randint(1, 100)  # 'alea' contient un nombre aléatoire entre 1 et 100	
				
from random import randint def tri_01(t): gauche = ... droite = ... while ... : if ... : ... else: ... return t def tableau_01(N): return ...

Lien vers les RÉPONSES

Le drapeau hollandais

L'algorithme précédent se transpose assez facilement au cas d'un tableau constitué d'éléments de 3 valeurs différentes uniquement, par exemple : [2, 0, 2, 1, 2, 0, 1]

Le but est donc de trier ce tableau, en gardant le même nombre d'éléments, de façon à ce que tous les 0 soient au début, suivis de tous les 1 et enfin de tous les 2 : [0, 0, 1, 1, 2, 2, 2].

Nous allons pour cela suivre un algorithme, appelé algorithme du drapeau hollandais ( cet algorithme a été inventé par E. Dijkstra, célèbre informaticien hollandais, pays dont le drapeau comporte 3 bandes colorées horizontales 🇳🇱 ).

Le principe est le suivant :

  • on utilise 3 variables :
    • gauche, qui délimite la "zone" des 0 ( les éléments en dessous de l'indice gauche sont tous des 0 ), initialisée à 0
    • milieu, qui sera utilisé pour parcourir le tableau, initialisée aussi à 0
    • droite, qui délimite la "zone" des 2 ( les éléments en dessus de l'indice droite sont tous des 2 ), initialisée à l'indice du dernier élément du tableau
    La "zone" des 1 sera donc située entre les indices gauche et droite.
  • à chaque étape de l'algorithme, on teste la valeur de l'élément du tableau présent à l'indice milieu :
    • si l'élément est un 0, on l'échange avec l'élément à l'indice gauche, et on incrémente gauche et milieu
    • si l'élément est un 1, on se contente d'incrémenter milieu
    • si l'élément est un 2, on l'échange avec l'élément à l'indice droite, et on décrémente droite.
Algo drapeau hollandais
  1. quelle condition est alors vérifiée lorsque le tableau est trié ?
  2. compléter la fonction drapeau_hollandais ci-dessous, qui prend en paramètre un tableau t constitué uniquement de 0, de 1 ou de 2, et qui le mute pour le trier selon l'algorithme précédent.
  3. écrire une fonction tableau_012 qui permet de créer aléatoirement un tableau ne contenant que des 0, des 1 ou des 2, de longueur N quelconque, passée en paramètre.
    Cette fonction sera écrite en une seule ligne, en utilisant une construction par compréhension judicieusement écrite.
  4. à l'aide de ces deux fonctions, tester le bon fonctionnement de l'algorithme, en affichant le tableau non trié, puis trié
from random import randint def drapeau_hollandais(t): gauche = ... milieu = ... droite = ... while ... : if ... : ... elif ... : ... else: ... return t def tableau_012(N): return ...

Lien vers les RÉPONSES

La vie scolaire

Dans un lycée, il y a 10 classes de seconde. L’effectif par classe est limité à 25 pour que les professeurs puissent assurer un suivi personnalisé, et les élèves travailler dans des conditions satisfaisantes (on peut rêver ).

Travail à faire

  1. Écrire l'instruction qui construit par compréhension un tableau de 10 éléments, chaque élément correspondant à l'effectif d'une classe, de valeur choisie aléatoirement entre 20 et 30.
  2. Écrire une fonction compte_surplus(L) qui prend en argument le tableau des effectifs, et qui renvoie, sous forme d'un autre tableau, le nombre d’élèves à enlever si l’effectif dépasse 25.
    Ex. : compte_surplus([24, 25, 26]) renvoie [0, 0, 1]
  3. Écrire une fonction repartition_possible(L) qui renvoie :
    • un tableau des modifications à effectuer dans le cas où c’est possible.
      Ex. ​ : repartition_possible([24, 25, 26]) renvoie [1, 0, − 1] )
    • si aucune répartition n'est possible, il faut créer une nouvelle classe et y placer tous les élèves en surplus.
      Ex. : repartition_possible([24, 25, 27]) renvoie [24, 25, 25, 2].
  4. Réfléchir à une modification de la fonction repartition_possible(L) pour que l’éventuelle classe créée ne soit pas « trop déséquilibrée » par rapport aux autres.
    Ex. : repartition_possible([24, 25, 27]) pourrait donner [19, 19, 19, 19].
def compte_surplus(L): pass return tab def repartition_possible(L): pass return L_modif

Lien vers les RÉPONSES