Copie par valeur ou par référence¶
Lorsqu'on fait b = a
Python
- recopie la valeur de
a
si son type est simple (immutable pour être plus précis). C'est le cas pour les booleens, réels, entiers, chaînes de caractères et tuples. - recopie l'adresse mémoire de
a
(qui est un pointeur) si le type est complexe (mutable). C'est le pour les listes, dictionnaires et ensembles.
Quelques petits problèmes¶
Voici 4 petits exemples qui illustrent cela avec des explications détaillées ensuite :
a = 3
b = a
b += 1
print(a, b)
3 4
Si ce premier vous a semblé normal (il l'est), alors que penser du suivant ?
On modifie y et x est aussi modifié :
x = [1,2,3,4]
y = x
y[0] = 8
print(x,y)
[8, 2, 3, 4] [8, 2, 3, 4]
Autre comportement surprenant :
x = [1,2,3,4]
for val in x:
val += 1
print(x)
[1, 2, 3, 4]
On a tout modifié et rien n'a été pris en compte !
Alors que pour le prochain, les modifications sont bien prises en compte...
dl = [ [1,2], [3,4] ]
for val in dl:
val += [8]
print(dl)
[[1, 2, 8], [3, 4, 8]]
Pointeurs et références¶
Pour comprendre ce qui s'est passé il faut comprendre comment sont stockées les variables en mémoire et ce qu'est un pointeur (une référence est un pointeur caché, en Python on a que des références).
Type compliqué (liste, dictionnaire...)¶
La mémoire d'un ordinateur peut être vue comme un grand vecteur où chaque case du vecteur à une adresse (adresse mémoire). Lorsque je crée une liste, [1,2,3,4]
, Python trouve un espace mémoire libre assez grand pour la stocker de facon contigue.
Lorsque j'assigne cette liste à x, x=[1,2,3,4]
, alors Python stocke dans x l'adresse mémoire où est stockée la liste (16 dans le dessin ci-dessous). Si ensuite je fais x=y
, il stocke dans y la valeur de x à savoir l'adresse mémoire de notre liste. Aussi lorsque je modifie la première valeur de y, y[0]=8
, c'est sans surprise que x est aussi modifié puisqu'ils font tous les deux référence à la même adresse mémoire (on
dit qu'ils pointent sur la même adresse mémoire).
On vient donc d'expliquer le deuxième cas, mais alors comment expliquer le premier cas.
Type simple (entier, réel...)¶
Une facon de voir les choses est d'imaginer que la valeur de l'entier ou du réel est stockée dans la case mémoire de la variable. C'est la cas dans certains langage mais pas en Python mais cette vision marche quand même et permet de comprendre les comportements qu'on a vu.
Ainsi b=a
est une copie de la valeur de a dans b. Les zones mémoires de a et b sont différentes.
Modifier b n'a donc pas d'impact sur la valeur de x.
Cela explique l'exemple 3. Lorsqu'on fait for val in x
on copie dans val
la valeur du n-ième élément de x
et donc modifier la valeur de val
avec val += 1
n'a pas d'impact sur le n-ième élement
de x
.
Dans l'exemple 4 le n-ième élement de x
est une liste, donc val
est un pointeur vers cette liste et modifier val
modifie le n-ième élément de x
.
Solutions pour modifier une liste¶
Pour changer une liste globalement, le plus propre est de créer une nouvelle liste à partir de l'ancienne :
x = [1,2,3,4]
x = [val+1 for val in x]
print(x)
[2, 3, 4, 5]
La solution naturelle pour les anciens du C ou du C++ est :
x = [1,2,3,4]
for i in range(len(x)):
x[i] += 1
print(x)
[2, 3, 4, 5]
Mais, outre le fait que cette solution est moins élégante (moins lisible), elle est surtout plus lente :
def fa(x):
return [val+1 for val in x]
def si(x):
for i in range(len(x)):
x[i] += 1
return x
x = list(range(1000))
%timeit fa(x)
%timeit si(x)
45.7 µs ± 3.18 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each) 87.1 µs ± 837 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)
On verra qu'avec Numpy on évite même les boucles pour travailler sur l'ensemble de la liste (x = x + 1
par exemple avec x un tableau)
{{ PreviousNext("../lesson1 Python basics/07 - Algorithmique et tris.ipynb", "02 - La porté des variables.ipynb") }}