Voila quelques essais d'un élève pour écrire une fonction qui modifie le score d'un joueur dans un jeu :
Comment interpréter le résultat affiché ?
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...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 :
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 :
Non, ça ne fonctionne toujours pas...comment interpréter tout cela ??
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 :
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.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 ).
Mais il est (de loin !) préférable de donner des noms différents aux variables locales et aux variables globales, même si celles-ci contiendront la même information; c'est une bonne pratique, qui facilite ( grandement ! ) le débogage et la maintenance d'un code...
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 :
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 :
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.
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...
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.
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 :
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 :
→ 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 :
Même sans le passer comme paramètre, le tableau score, modifié dans la fonction, reste modifié dans le programme principal !!
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 ) :
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...
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.
Copier deux variables, on sait faire : il suffit d'utiliser l'opérateur d'affectation =
:
Que constatez-vous ? D'après ce qui a été dit juste avant, est-ce normal ?
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 :
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.
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 !!
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.
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.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.
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. 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 ?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 :
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.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.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
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 :
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.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 ).
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.compte_surplus([24, 25, 26])
renvoie [0, 0, 1]
repartition_possible(L)
qui renvoie :
repartition_possible([24, 25, 26])
renvoie [1, 0, − 1]
)repartition_possible([24, 25, 27])
renvoie [24, 25, 25, 2]
.repartition_possible(L)
pour que l’éventuelle classe créée ne soit pas « trop déséquilibrée » par rapport aux autres.