Salut à toi, élève de NSI, comment puis-je t'aider ?
Tout ce qui a été dit sur la portée des variables 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 l'exemple de la modification d'un score, 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 Lolviz ( 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.