Nous abordons ici un sujet assez ardu qui n'est pas spécifique aux listes, ni aux dictionnaires. La raison pour laquelle nous en parlons ici est qu'il nous permettra de comprendre ce qu'il se passe lorsque l'on copie une liste dans une autre.
Nous allons voir que pour deux variables L1 et L2 de type liste, une affectation du type L2 = L1, ne recopie pas réellement la valeur de L1 dans L2 !
Mais de manière générale, la mutabilité est un concept qui touche à la représentation mémoire des variables en Python, quelque soit leur type. Nous allons donc dépasser ici le sujet de ce cours et revoir également certaines parties du cours sur les sous-programmes. En particulier, la portée des variables et le passages de paramètres.
La notion de variable telle que nous l'avions définie dans le premier cours n'était qu'une première approximation.
Nous avions dit qu'une variable est une plage mémoire possédant un nom (celui de la variable) et que cette plage mémoire sert à mémoriser la valeur de la variable. En réalité, la représentation d'une variable en Python est un peu plus complexe.
Tout d'abord, la valeur d'une variable peut être stockée dans plusieurs plages mémoires. C'est le cas notamment des listes et des dictionnaires. Mais pour simplifier les explications, nous allons considérer ici que la valeur d'une variable est toujours mémorisée dans une seule plage mémoire possédant une taille et une adresse.
Deuxièmement, l'interprèteur Python possède à tout moment des informations sur les variables du programme interprèté. Ces informations ne se limitent pas au nom et à la valeur de la variable. En particulier, on y trouvera également:
Il y a donc d'une part, la plage mémoire qui contient l'identifiant de la variable et d'autre part, celle qui contient sa valeur.
Par exemple, pour une variable a de type entier et de valeur 1961 (dans cette figure, ainsi que dans toutes les figures qui suivent, les identifiants sont des nombres que nous avons choisi arbitrairement) :
Pour retrouver la valeur d'une variable, l'interprèteur procède donc en deux étapes: il va d'abord retrouver la valeur de l'identifiant et en second lieu retrouver, à partir de cet identifiant, l'adresse de la plage mémoire contenant la valeur. Le type de la variable lui permettra à ce moment de savoir combien d'octets ont été reservés pour cette valeur.
Les types de variable en python se divisent en deux catégories: les types mutables et les types immutables.
Vous connaissez pour l'instant trois types immutables: les nombres, les chaines de caractères et les booléens. Les valeurs des variables de ce type ne peuvent être modifiées que par une affectation.
Par contre les listes sont mutables. Toutes les opérations de modification d'une liste le démontrent : adjonction d'un nouvel élément (append), suppression d'une élément (del), ... Toutes ces opérations ne sont pas des affectations !
De plus, la valeur de l'élément particulier d'une liste peut être modifiée par l'affectation d'une nouvelle valeur à cet élément. Indirectement, cela modifie également la valeur de la liste. Bien qu'il s'agisse d'une affectation, cela montre également que les listes sont mutables, car le membre gauche de cette affectation n'est pas la liste elle même.
Exemple: une liste L a pour valeur [1,9,6,1]. Pour modifier cette valeur en [1,9,6,4], on est pas obligé de faire L = [1,9,6,4], puisque L[3]=4 convient également.
Lorsque la valeur d'une variable change via une affectation dont le membre gauche est le nom de cette variable, une nouvelle plage mémoire est allouée pour la nouvelle valeur et l'identifiant de la variable change nécessairement.
Cela est vrai aussi bien pour les types mutables que pour les types immutables. Exemples:
Par contre, toute modification de la valeur d'une variable par une autre opération conserve l'identifiant. Ceci est impossible pour une variable de type immutable et possible pour une variable de type mutable. Par conséquent, pour une variable de type mutable, l'identifiant ainsi que la plage mémoire allouée à la valeur de cette variable peuvent rester inchangées, même lorsque l'on change la valeur de cette variable.
Exemples:
Une affectation du type b = a recopie en principe la valeur de la variable a dans la variable b. En réalité, ce n'est pas la valeur qui est recopiée, mais l'identifiant!
Par exemple avec a = 1961, puis b = a on obtient la situation suivante:
Comme les identifiants de a et b sont identiques, ils définissent la même plage mémoire pour les valeurs de ces deux variables. Autrement dit, a et b ont la même valeur.
Cette manière de procéder permet un gain de temps (copie rapide) et de mémoire (une seule plage mémoire pour deux variables). Mais elle pose un problème technique : si le contenu de la plage mémoire de la valeur de a est modifié cela entraine automatiquement la modification de la valeur de b !
Pour le programmeur Python non averti, cet effet de bord, peut provoquer une erreur de programmation difficile à élucider.
C'est ici qu'intervient la mutabilité car nous allons voir que ceci ne peut se produire que si a et b sont de type mutable.
C'est par exemple le cas pour des listes. Prenons comme exemple les instructions suivantes:
L1 = [1,9,6,1] L2 = L1
Après l'exécution de ces deux instructions, la représentation mémoire de L1 et L2 sera (la valeur de l'identifiant est arbitraire):
Si à ce moment là, l'instruction suivante est exécutée:
L2[2:4]=[3,5]
elle modifie la valeur de L2 en [1,9,3,5], mais aussi celle de L1 !.
En effet, comme le membre gauche de l'affectation L2[2:4]=[3,5] n'est pas L2, il n'y a pas de nouvelle plage mémoire allouée pour cette valeur. Mais comme c'est également la plage mémoire allouée à la valeur de L1, la valeur de L1 est également modifiée en [1,9,3,5].
Voyons à présent pourquoi cet effet de bord ne peut pas se produire pour une variable de type immutable, en prenant comme exemple les variables numériques. Après les instructions:
a = 1961 b = a
Nous avons la situation suivante:
Comme b est immutable, le seul moyen de changer sa valeur est de lui affecter une nouvelle valeur. Par exemple:
b = 1935
Or nous savons qu'affecter une nouvelle valeur à une variable (de type immutable ou mutable) provoque obligatoirement l'allocation d'une nouvelle plage mémoire pour cette valeur. Autrement dit, l'identifiant change et la plage mémoire contenant la valeur n'est plus la même. Par conséquent, l'affectation b = 1935, n'a aucun effet sur la valeur de a.
A ce stade, vous vous demandez peut être comment réaliser une copie de liste sans risquer un effet de bord. Il existe heureusement une solution: importez le module copy et utilisez la fonction deepcopy.
Exemple:
from copy import * L1=[1,9,6,1] L2 = deepcopy(L1)
La fonction deepcopy retourne une véritable copie stockée dans une plage mémoire différente. De ce fait, aucun effet de bord n'est possible.
Par exemple, si nous mofifions à ce stade L2 en [1,9,3,5] de la manière suivante:
L2[2:4] = [3,5]
la liste L1 n'est pas modifiée (voir la figure).
Nous avons à présent une vision plus exacte de la représentation des variables en Python. Nous allons revoir ici le mécanisme de passage des paramètres en fonction de cette représentation.
En réalité, le mode de passage de paramètres utilisé par Python n'est pas le mode de passage par valeur.
Il y a deux cas à distinguer selon la nature du paramètre effectif:
Concentrons nous à présent sur le premier cas. En principe, le passage par référence permet de modifier la valeur du paramètre effectif. En Python, les choses sont malheureusement un peu plus compliquées ! : le paramètre effectif ne peut être modifier que s'il est de type mutable.
Voyons ceci plus en détails avec deux exemples.
Considérons le sous-programme suivant:
def ChangerValeur (L2) : L2[2:4]=[3,5]
Affectons la valeur [1,9,6,1] à une liste L1, puis appelons ce sous-programme avec L1 comme paramètre effectif:
L1 =[1,9,6,1] ChangerValeur(L1)
En exécutant ce programme, on constate que la valeur de L1 est modifiée en [1,9,3,5]. Il se passe ici exactement la même chose que dans la copie de variable L2=L1. Le passage par référence copie l'identifiant de L1 dans celui de L2. Les valeurs de ces deux variables sont donc enregistrées dans la même plage mémoire. L'affectation L2[2:4]=[3,5], ne créé pas une nouvelle plage mémoire pour L2. Elle agit donc directement sur la plage mémoire associée à L1.
Remarque: si l'affectation du sous-programme ChangerValeur avait été L2 = [1,9,3,5], la liste L1 serait restée inchangée. En effet, comme nous l'avons déjà mentionné, avec une affectation dont le membre gauche est un simple nom de variable, l'affectation d'une nouvelle valeur implique nécessairement l'allocation d'une nouvelle plage mémoire pour la valeur de cette variable.
Considérons le sous-programme suivant:
def ChangerValeur (b) : b = 1935
Affectons la valeur 1961 à une variable a, puis appelons ce sous-programme avec a comme paramètre effectif:
a = 1961 ChangerValeur(a)
En exécutant ce programme, on constate que la valeur de a n'est pas modifiée !
Cela peut s'expliquer de la manière suivante: comme il y a passage par référence, l'identifiant de b est le même que celui de a avant l'exécution de l'instruction b = 1935. Par contre, lorsque cette affectation est exécutée une nouvelle plage mémoire est allouée à la valeur de la variable b. L'identifiant de b va donc changer et la valeur 1935 sera stockée dans la nouvelle plage mémoire. Par conséquent, la valeur de a ne sera pas changée.
De manière générale, un paramètre effectif de type immutable ne pourra jamais être modifié par appel de sous-programmme, car tout changement de valeur d'une variable de type immutable implique l'allocation d'une nouvelle plage mémoire pour cette nouvelle valeur.
La mutabilité a également un rôle à jouer dans la portée des variables. En effet, la règle que nous avions énoncé sur la portée en écriture des variables globales n'est pas vraie pour les variables de type mutable.
En réalité, les variables globales de type mutables ont une portée plus grande: la valeur d'une variable de ce type peut être modifiée dans tout le fichier source: le programme principal et tout les sous-programmes. Pour pouvoir agir sur une telle variable de depuis un sous-programme, il n'est pas forcément nécessaire de la déclarer en global.
Tout dépend de la méthode utilisée pour modifier la valeur de la variable. S'il s'agit d'une affectation dont le membre gauche est le nom de la variable, alors la déclaration en global est nécessaire. En effet dans ce cas, comme nous l'avions déjà vu, un changement de valeur implique nécessairement l'allocation d'une nouvelle plage mémoire pour cette nouvelle valeur. Par contre, pour une variable de type mutable, la modification de valeur est possible par d'autres moyens. Dans ce cas, la déclaration en global n'est pas nécessaire.
def ModifierLaListe (): L[3] = 11 L = [5,18,9,3] ModifierLaListe ()
Dans cet exemple, l'appel de ModifierLaListe remplace le 4ème élement de la liste L par 11. La variable L est bien modifiée, bien qu'elle ne soit pas déclarée en global.
On aurait constaté le même phénomène (modification de L sans que L soit déclarée en global), avec toutes les opérations sur les listes (pop, del , etc ...).
Par contre, cela ne fonctionne pas avec une affectation du type L = ...
Par exemple:
def ModifierLaListe (): L = [5,18,9,11] L = [5,18,9,3] ModifierLaListe ()
Ici, l'appel de ModifierLaListe est sans effet !