Quand on affecte une valeur à une variable (par exemple x = 2013), on crée en fait un pointeur (une adresse) pour désigner cette valeur qu’il faut imaginer quelque part en mémoire.
Les variables Python sont donc des références (des adresses, des pointeurs) vers des valeurs. Pour connaître l’adresse où se trouve exactement la valeur qu’on a associée à un identificateur, on utilise la fonction intégrée id.
L’égalité structurelle de deux objets signifie l’égalité de leurs valeurs, alors que l’égalité physique signifie l’égalité des adresses de ces valeurs (bien sûr l’égalité physique implique l’égalité structurelle, mais la réciproque est fausse).
Pour tester l’égalité structurelle, on utilise l’opérateur ==, et pour l’égalité physique, on utilise l’opérateur is.
Ces distinctions ne sont pas très importantes quand on manipule des objets dits “non mutables” (comme les types de base : entiers, flottants, nombres complexes, chaînes de caractères, et également les tuples qui seront étudiés plus tard).
En revanche, la distinction entre égalité physique et égalité structurelle prend tout son sens quand on modifie les éléments d’objets “mutables” (comme les listes ou les dictionnaires).
Le mieux est de prendre des exemples, en commençant par du très simple :
|
1 2 3 4 5 6 7 8 9 10 |
>>> x = 2013 # on initialise la variable x avec la valeur 2013. >>> id(x) # voici exactement à quelle adresse de la mémoire se trouve la valeur 2013 4343755504 >>> y = 2013 # on initialise la variable y avec la même valeur 2013. >>> id(y) # on voit que les deux valeurs 2013 sont à des adresses différentes en mémoire 4343774064 >>> x == y # on demande si les valeurs sont égales, la réponse est oui True >>> x is y # on demande si les valeurs sont à la même adresse, la réponse est non False |
Continuons sur la lancée de l’exemple précédent.
|
1 2 3 4 5 |
>>> y = x # on recopie la variable x dans la variable y >>> (id(x), id(y)) # x et y se réfèrent à la même valeur, à un endroit précis de la mémoire (4343755504, 4343755504) >>> (x is y, x == y) # il y a égalité physique, donc égalité structurelle (True, True) |
Continuons, en modifiant maintenant le contenu de la seule variable y :
|
1 2 3 4 5 6 7 |
>>> y += 1 # on ajoute 1 au contenu de la variable y >>> (x,y) # y contient maintenant 2014, mais x a conservé sa valeur 2013 (2013, 2014) >>> (id(x), id(y)) # x pointe vers la même adresse, mais pas y (4343755504, 4343773200) >>> (x == y, x is y) # il n'y a plus égalité structurelle, et encore moins égalité physique (False, False) |
On va maintenant effectuer des opérations analogues, mais sur des listes (les objets de type liste seront étudiés en détail un peu plus loin) :
|
1 2 3 |
>>> x = [1,2,3]; y = [1,2,3] # on met la liste [1,2,3] dans x, puis dans y >>> (x is y, x == y) # les listes ne sont pas à la même adresse, mais ont la même valeur (False, True) |
On continue sur cet exemple, en redéfinissant la liste y
|
1 2 3 |
>>> y = y + y # on concatène la liste y à elle-même >>> (x, y) # x et y contiennent donc deux listes de valeurs différentes ([1, 2, 3], [1, 2, 3, 1, 2, 3]) |
On va maintenant recopier à nouveau x dans y, mais modifier seulement un élément de y.
|
1 2 3 4 5 6 7 8 9 10 |
>>> y = x # on recopie le contenu de x (la liste [1,2,3]) dans la variable y >>> (id(x), id(y)) # x et y pointent sur une même adresse en mémoire (4361208752, 4361208752) >>> y[0] = 999 # on place 999 en position 0 (le 1er élément) de la liste contenue dans y >>> (x, y) # on voit que la modification a été répercutée sur la liste contenue dans x !! ([999, 2, 3], [999, 2, 3]) >>> (id(x), id(y)) # en fait les variables x et y pointent toujours sur la même adresse (4361208752, 4361208752) >>> x is y # il s'agit donc d'égalité physique True |
L’exemple précédent est important. Les listes Python sont des objets composites (formés d’éléments a priori disparates mais rassemblés dans une même structure). Si on modifie un élément d’une liste, Python ne modifie pas l’adresse de celle-ci (en fait, il modifie seulement l’adresse de l’élément concerné dans cette liste).
Cela explique qu’après l’instruction y = x (à l’issue de laquelle x et y pointent sur une même liste en mémoire), la modification y[0] = 999 semble répercutée sur la liste contenue dans la variable x (tout simplement car les variables x et y continuent à pointer sur la même adresse).
L’instruction y = x n’a donc pas associé à y une nouvelle copie de la liste associée à x, elle a fait pointer y et x sur une même adresse (toute modification d’un des deux éléments x[k] ou y[k] sera donc “répercutée” sur l’autre).
Si on veut créer une copie d’une liste qui soit indépendante de l’original, on procédera de la façon suivante :
|
1 2 3 4 5 6 7 8 9 10 11 |
>>> x = [1,2,3] # on place une liste dans la variable x >>> y = x[:] # on place dans y une copie indépendante de cette liste >>> (x, y) # les deux valeurs sont identiques ([1, 2, 3], [1, 2, 3]) >>> (id(x),id(y)) # mais les listes ne sont pas à la même adresse (4298413336, 4361210768) >>> (x is y, x == y) # il y a égalité structurelle, mais pas égalité physique (False, True) >>> y[0] = 999 # on modifie le premier élément de la liste y >>> (x, y) # mais ça ne se répercute plus sur la liste x ([1, 2, 3], [999, 2, 3]) |
Remarque : l’expression x[:] est un cas particulier de la syntaxe x[a:b] qui renvoie la liste des éléments de l’objet x situés de la position a (incluse) à la position b (exclue). Par défaut a vaut 0 (c’est le début de l’objet x) et b vaut la longueur de cet objet. Ainsi x[:] renvoie donc la totalité de l’objet x (mais cette copie est indépendante de l’original).
Attention : la notion de “copie originale”, telle qu’elle a été évoquée ci-dessus (l’instruction y = x[:]) n’est tout à fait exacte que si la liste source (ici x) ne contient que des objets “non mutables” (nombres, chaînes, tuples). En effet, après la copie, l’adresse de y est différente de celle de x, mais l’adresse de chaque y[i] reste celle de x[i].
Un exemple permettra d’y voir plus clair :
|
1 2 3 4 5 6 7 8 9 10 11 |
>>> x = [0,[10,20],2,[30,31,32]] # on fabrique une liste composite >>> [id(x),[id(t) for t in x]] # l'adresse de x, et celles des x[i] successifs [4318592032, [4297261120, 4328417832, 4297261184, 4359646240]] >>> y = x[:]; y # on effectue une copie "originale" de x dans y [0, [10, 20], 2, [30, 31, 32]] >>> [id(y),[id(t) for t in y]] # l'adresse de y est nouvelle, pas celle des y[i] [4359645448, [4297261120, 4328417832, 4297261184, 4359646240]] >>> y[0] = 1000; y[1][0] = 2013; y # modifions y[0] (non mutable) et y[1] (mutable) [1000, [2013, 20], 2, [30, 31, 32]] >>> x # pas d'impact sur x[0], mais impact sur x[1] [0, [2013, 20], 2, [30, 31, 32]] |