Python : affectation vs. copy vs. deepcopy

1 – Introduction

Quand on écrit un programme, on a souvent besoin de sauvegarder le contenu d’une variable dans une autre variable avant de la modifier. Ou alors, on construit une variable à partir du contenu d’une autre variable et changer uniquement les parties qui diffèrent. Là encore, on a besoin de copier une variable dans une autre.

En python, les choses ne sont pas aussi simples. En effet, il y a trois niveaux de copies qu’on va détailler dans cet article.

2 – Différents niveaux de copie

Avant d’expliquer en détail, il serait intéressant de présenter certaines fonctions Python qui nous seront utiles.

  • La fonction prédéfinie « id » :

En regardant l’aide donné par le shell python, et ce, en faisant « help(id) », après traduction on aura ça :

id(obj, /)
Renvoie l’identité d’un objet.


Ceci est garanti d’être unique parmi tous les objets existants simultanéments.
(CPython utilise l’adresse mémoire de l’objet.)

C’est important de comprendre ce que renvoie la fonction « id » car on va l’utiliser juste après pour mieux expliquer les différentes manières de copier des variables.

2.1 – Affectation

Pour comprendre de quoi il s’agit, voici un exemple de code python : 

>>> l1 = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
>>> l2 = l1

La deuxième ligne de ce code crée un nouvelle référence au même objet liste contenant les éléments de 1 jusqu’à 10. Il est important de souligner qu’il n’y a qu’un seul objet liste référencé par deux références : « l1 » et « l2 ».

On peut le vérifier en affichant l’identité des objets référencés « l1 » et « l2 » en faisant :

>> print(id(l1), id(l2), sep="\n")
45511432
45511432

Le résultat varie d’une exécution à une autre mais ce qui est sûr, c’est que la valeur est la même pour les deux références.

Une autre manière de vérifier qu’il n’y a qu’un seul objet référencé par les deux références, c’est en modifiant le contenu de « l1 », par exemple, puis voir le contenu de « l2 » :

>>> l1[0] = 100
>>> print(l1, l2, sep="\n")
[100, 2, 3, 4, 5, 6, 7, 8, 9, 10]
[100, 2, 3, 4, 5, 6, 7, 8, 9, 10]

On voit bien que c’est le même résultat pour les deux références, car, tout simplement, c’est le même objet qui a été modifié.

La figure suivante illustre bien ce qui se passe qu’on fait une affectation d’un objet à une variable (référence).

Figure 1 : Illustration d’un affectation

2.2 – copie simple (copy.copy)

Comme indiquer dans le titre, la fonction « copy » est définie dans le module python « copy ». Donc, pour pouvoir l’utiliser il faudra préalablement importer ce module en faisant :

import copy

Reprenons maintenant l’exemple précédent, et voyons ce qui se passe si on utilise la fonction « copy » au lieu d’une affectation :

>>> l1 = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
>>> l2 = copy.copy(l1)
>>> print(id(l1), id(l2), sep="\n")
45511432
45833384

Remarquez ici, que les identités (donc les adresses) des objets référencés par « l1 » et « l2 » sont différentes, donc ça montre bien que ce sont deux objets distincts.

Prenant un autre exemple :

>>> l1 = [[1, 2, 3], [4, 5, 6]]
>>> l2 = copy.copy(l1)
>>> print(id(l1[0]), id(l2[0]), sep="\n")
45833640
45833640
>>> print(id(l1[1]), id(l2[1]), sep="\n")
45833544
45833544

Ceci montre que le premier élément (respectivement le deuxième élément) de chacune des deux listes « l1 » et « l2 » fait référence au même objet liste « [1, 2, 3] » (respectivement « [4, 5, 6] »). Si jamais on met à jour la valeur de « l1[0][0] », « l2[0][0] » sera modifié également.

La figure suivante illustre bien ce qui passe qu’on crée une copie en utilisant la fonction « copy.copy ».

Figure 2 : Illustration d’un copie simple avec « copy.copy »

La situation qu’on a vu précédemment peut être parfois gênante quand on a plusieurs niveaux de listes (c-à-d liste de listes de listes de …. ) ou d’objets de manière générale. En effet, il n’y a que le premier niveau (première liste) qui est dupliquer, mais les autres niveaux ne le sont pas.

Pour pallier à ce petit problème, il existe une manière pour faire une copie profonde qu’on va détailler juste après.

2.3 – copie profonde (dopy.deepcopy)

On a vu précédemment, « copy.deepcopy » permet de pallier à la limite de la copie simple en dupliquant tous les niveaux de listes (ou d’objets de manière générale). Ceci est fait de manière récursive dans le but de créer un nouvel objet identique au premier (Les deux ont les mêmes valeurs) mais qui sont totalement distincts.

Reprenons l’exemple précédent :

>>> l1 = [[1, 2, 3], [4, 5, 6]]
>>> l2 = copy.copy(l1)
>>> print(id(l1[0]), id(l2[0]), sep="\n")
45833448
33401128
>>> print(id(l1[1]), id(l2[1]), sep="\n")
45833384
45631656

Remarquez bien que les éléments des deux listes référencent des objets « liste » distincts. Car les valeurs de leurs identités sont différentes.

La figure suivante illustre bien ce qui se passe quand on fait une copie profonde avec « copy.deepcopy ».

Figure 3 : Illustration d’une copie profonde (deepcopy)

3 – Conclusion

Le but de cet est de bien montrer et expliquer la différence entre les trois niveaux de copies (affectation, copie simple, copie profonde). C’est très important de maîtriser ces notions pour éviter d’avoir des surprises à l’exécution d’un programme.