3.4. Mutabilité¶
Il s’agit d’un paragraphe un peu subtil : il s’agit d’expliquer la différence fondamentale qu’il existe en Python entre les objets que l’on peut modifier (listes) ou que l’on ne peut modifier (tuples ou chaînes de caractère).
Considérons ce premier exemple où les variables sont des entiers.
In [1]: a = 1
In [2]: b = a
In [3]: a = 2 # on modifie a
In [4]: a
Out[4]: 2
In [5]: b # b n'a pas ete modifiée
Out[5]: 1
Considérons maintenant l’exemple suivant où les variables sont des listes.
In [6]: a = [1, 2, 3]
In [7]: b = a
In [8]: a[0] = 'foo' # on modifie la liste a
In [9]: a
Out[9]: ['foo', 2, 3]
In [10]: b # la liste b a aussi ete modifiée !
Out[10]: ['foo', 2, 3]
Pour expliquer la différence entre ces deux exemples, il faut comprendre la représentation des objets Python en mémoire. Pour cela, on va utiliser la fonction id
. Pour schématiser, celle-ci renvoie l’emplacement en mémoire d’un objet.
In [11]: a = 1
In [12]: b = a
In [13]: id(a), id(b) # les variables a et b pointent vers le même emplacement en mémoire
Out[13]: (140703447966504, 140703447966504)
In [14]: a = 2
In [15]: id(a), id(b) # la variable b pointe toujours vers le même emplacement mais plus la variable a
Out[15]: (140703447966536, 140703447966504)
L’instruction a = 2
a fait pointer la variable a
vers un autre emplacement en mémoire où est stocké l’entier 2
.
In [16]: a = [1, 2, 3]
In [17]: b = a
In [18]: id(a), id(b) # les variables a et b pointent vers le même emplacement en mémoire
Out[18]: (2223623471680, 2223623471680)
In [19]: a[0] = 'foo'
In [20]: id(a), id(b) # les variables a et b pointent toujours vers le même emplacement
Out[20]: (2223623471680, 2223623471680)
Ici, l’instruction a[0] = 'foo'
a modifié l’objet stocké à l’emplacement commun vers lequel pointent les variables a
et b
. Comme a
et b
pointent toujours le même emplacement en mémoire, la variable b
est maintenant associée à ce nouvel objet.
Mais pourquoi cette différence de comportement ? Il existe en Python deux types d’objets : les objets mutables et les objets immutables. On peut donner la définition suivante.
Un objet est dit mutable si on peut changer sa valeur après sa création. Il est dit immutable dans le cas contraire [1].
- Objets immutables
Entiers, flottants, complexes, tuples, chaînes de caractères, …
- Objets mutables
Listes, dictionnaires, …
Voilà la solution du mystère : toutes les variables pointant vers un même objet mutable sont affectées par la modification de cet objet. Ceci ne peut pas se produire lorsque des variables pointent vers un objet immutable puisque celui-ci ne peut-être modifié.
Note
Bien souvent, on veut copier une liste dans un nouvel objet pour qu’il ne subisse pas les modifications de l’objet initial. Pour cela, il ya plusieurs possibilités :
le slicing
[:]
;l’utilisation de la méthode
copy
;l’utilisation du constructeur
list
.
In [21]: liste1 = [1, 2, 3]
In [22]: liste2 = liste1[:]
In [23]: liste3 = liste1.copy()
In [24]: liste4 = list(liste1)
In [25]: id(liste1), id(liste2), id(liste3), id(liste4) # les objets sont bien distincts
Out[25]: (2223623509120, 2223623509248, 2223623510592, 2223623510784)
In [26]: liste1[0] = 'toto'
In [27]: liste1, liste2, liste3, liste4 # liste1 a ete modifiée mais pas les autres listes
Out[27]: (['toto', 2, 3], [1, 2, 3], [1, 2, 3], [1, 2, 3])
Les opérateurs +
et +=
Le lecteur attentif aura remarqué qu’on semblerait pouvoir modifier un objet immutable telle qu’une chaîne de caractères ou un tuple à l’aide des opérateurs +
ou +=
. Mais ces opérateurs ne modifient pas l’objet en question ; ils créent en fait un nouvel objet. On peut s’en convaincre à l’aide de la fonction id
.
In [28]: t = (1, 2, 3)
In [29]: id(t)
Out[29]: 2223623345152
In [30]: t = t + (4, 5)
In [31]: t
Out[31]: (1, 2, 3, 4, 5)
In [32]: id(t)
Out[32]: 2223612866816
In [33]: t = (1, 2, 3)
In [34]: id(t)
Out[34]: 2223613089984
In [35]: t += (4, 5)
In [36]: t
Out[36]: (1, 2, 3, 4, 5)
In [37]: id(t)
Out[37]: 2223613286400
Pour les objets mutables tels que les listes, les opérateurs +
et +=
se comportent de manières différentes : l’opérateur +
crée un nouvel objet tandis que l’opérateur +=
modifie l’objet initial.
In [38]: liste1 = [1, 2, 3]
In [39]: liste2 = liste1
In [40]: liste1 = liste1 + [4, 5]
In [41]: liste1, liste2 # seule liste1 a ete modifiée
Out[41]: ([1, 2, 3, 4, 5], [1, 2, 3])
In [42]: id(liste1), id(liste2) # c'est normal : liste1 et liste2 pointent vers des objets distincts
Out[42]: (2223623640448, 2223623567872)
In [43]: liste1 = [1, 2, 3]
In [44]: liste2 = liste1
In [45]: liste1 += [4, 5]
In [46]: liste1, liste2 # liste1 et liste2 ont ete modifiées
Out[46]: ([1, 2, 3, 4, 5], [1, 2, 3, 4, 5])
In [47]: id(liste1), id(liste2) # c'est normal : liste1 et liste2 pointent vers le même objet
Out[47]: (2223623688128, 2223623688128)
Egalité structurelle ou physique
On a vu que l’opérateur ==
permettait de tester si deux objets étaient égaux. Mais de quel type d’égalité parle-t-on alors ? L’opérateur ==
teste si deux objets ont la même valeur sans pour autant qu’il partage le même emplacement en mémoire. On parle alors d\”égalité structurelle.
Lorsque « deux » objets sont en fait identiques (c’est-à-dire lorsqu’ils ont le même emplacement en mémoire), on parle d\”égalité physique. Pour tester l’égalité physique, on peut comparer les emplacements en mémoire à l’aide de la fonction id
ou plus simplement utiliser l’opérateur is
.
In [48]: liste1 = [1, 2, 3]
In [49]: liste2 = liste1
In [50]: liste3 = liste1[:]
In [51]: liste1, liste2, liste3
Out[51]: ([1, 2, 3], [1, 2, 3], [1, 2, 3])
In [52]: id(liste1), id(liste2), id(liste3)
Out[52]: (2223623695232, 2223623695232, 2223623696896)
In [53]: liste2 == liste1, liste3 == liste1
Out[53]: (True, True)
In [54]: liste2 is liste1, liste3 is liste1
Out[54]: (True, False)
Un exemple peut-être un peu plus surprenant.
In [55]: [1, 2, 3] == [1, 2, 3]
Out[55]: True
In [56]: [1, 2, 3] is [1, 2, 3]
Out[56]: False
Python a en fait stocké deux versions de la liste [1, 2, 3]
dans deux emplacements en mémoire distincts.
On termine par un cas plus vicieux que les deux exemples initiaux et qui peut faire passer des nuits blanches au programmeur débutant en Python.
In [57]: a = [[0] * 3] * 4
In [58]: a
Out[58]: [[0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0]]
In [59]: a[0][0] = 1 # on pense n'avoir modifié qu'un élément de la liste de listes a
In [60]: a # en fait non...
Out[60]: [[1, 0, 0], [1, 0, 0], [1, 0, 0], [1, 0, 0]]
Notes