Participer au site avec un Tip
Rechercher
 

Améliorations / Corrections

Vous avez des améliorations (ou des corrections) à proposer pour ce document : je vous remerçie par avance de m'en faire part, cela m'aide à améliorer le site.

Emplacement :

Description des améliorations :

Dupliquer une collection Python

Proprement afficher vos collections Bloc d'instructions et indentation


Accès rapide :
La vidéo
Présentation de la problématique
Duplication d'une collection (de type Shallow Copy)
Appel à la méthode copy
Appel au constructeur de la collection
Utilisation des list/set/dict en compréhension
Qu'entend-on par copie peu profonde (Shallow Copy en anglais) ?
Duplication d'une collection (de type Deep Copy)
Utilisation du module JSON
Utilisation du module copy
Travaux pratiques
Les énoncés
Les corrections

La vidéo

Cette vidéo vous explique comment cloner correctement une collection Python (list, tuple, set ou dictionnaire).


Dupliquer une collection en Python.

Présentation de la problématique

Pour comprendre ce qui va suivre, commençons par analyser le programme Python suivant. A votre avis, que va-t-il afficher ?

 1 
 2 
 3 
 4 
 5 
 6 
 7 
 8 
 9 
 10 
 11 
# Nous avons une première collection (une liste).
data = [10, 20, 30, 40, 50]
# Nous souhaiterions en avoir une seconde contenant les mêmes données.
data2 = data

# Nous cherchons maitenant à modifier le contenu de data2.
data2[2] = 3000

# Et nous affichons les deux collections afin de valider le contenu de chacune.
print("data2 =", data2)        # Je pense qu'il n'y a pas de doute à ce niveau.
print("data =", data)         # Par contre là, c'est plus discutable !    :-) 
Un premier exemple pour comprendre la problématique

Qu'en pensez-vous ? Pour la dernière ligne, l'appel à print affiche [10, 20, 30, 40, 50] ou [10, 20, 3000, 40, 50] ? Effectivement, on peut envisager les deux cas :

Allez, je termine le suspens et la bonne réponse est ... à votre avis ... bon, ok je suis lourd ... la seconde affirmation ! Voici les résultats affichés par le programme précédent.

data2 = [10, 20, 3000, 40, 50]
data = [10, 20, 3000, 40, 50]

Voici un diagramme montrant mieux ce qui se passe en mémoire.

Duplication de la variable (de l'adresse de la collection) et non pas de la collection.

Pour information, la fonction id affiche l'adresse mémoire d'une donnée Python portée par une variable. Je couple cet appel avec un autre à la fonction hex me permettant d'afficher l'adresse en hexadécimal (c'est l'usage). Voici un exemple de code validant le diagramme précédent.

>>> data = [10, 20, 30, 40, 50]
>>> data2 = data
>>> print(hex(id(data)))
0x7f6572aea6c0
>>> print(hex(id(data2)))
0x7f6572aea6c0
>>> 
On affiche les adresses mémoires des données portées par les variables data et data2.
il en va de même pour les autres classes de collections. Vous auriez dupliqué la variable et son pointeur, mais pas la zone pointée (la collection).

Mais alors, comment dupliquer une collection ?

Duplication d'une collection (de type Shallow Copy)

En fait, il y a plusieurs manières de dupliquer une collection. Prenons le temps de les regarder une à une.

Appel à la méthode copy

Si l'on considère la classe list et si vous en affichez les méthodes disponibles vous verrez que la classe propose une méthode copy.

>>> dir(list)
['__add__', '__class__', '__class_getitem__', '__contains__', '__delattr__', '__delitem__', '__dir__', 
'__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getitem__', '__gt__', '__hash__', 
'__iadd__', '__imul__', '__init__', '__init_subclass__', '__iter__', '__le__', '__len__', '__lt__', 
'__mul__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__reversed__', '__rmul__', 
'__setattr__', '__setitem__', '__sizeof__', '__str__', '__subclasshook__', 'append', 'clear', 'copy', 
'count', 'extend', 'index', 'insert', 'pop', 'remove', 'reverse', 'sort']
>>> 
Les méthodes exposées par la classe liste.

Cette méthode permet donc de copier une liste comme en atteste l'exemple de code suivant.

 1 
 2 
 3 
 4 
 5 
 6 
 7 
 8 
 9 
 10 
 11 
# Nous avons une première collection (une liste).
data = [10, 20, 30, 40, 50]
# Nous produisons une copie de data dans data2.
data2 = data.copy()

# Nous cherchons maintenant à modifier le contenu de data2.
data2[2] = 3000

# Et nous affichons les deux collections afin de valider le contenu de chacune.
print("data =", data)
print("data2 =", data2)
Duplication d'une liste via la méthode list.copy.

Et voici les résultats produits par cet exemple.

data = [10, 20, 30, 40, 50]
data2 = [10, 20, 3000, 40, 50]
malheureusement, une méthode copy n'est pas présente sur toutes les classes Python : c'est notamment le cas de la classe date (ce qui en soit n'est pas si grave, car ce type est immuable). Mais il est possible d'utiliser la fonction copy proposée par le module du même nom pour dupliquer cette donnée.
 1 
 2 
 3 
 4 
 5 
 6 
 7 
 8 
 9 
 10 
 11 
from copy import copy
from datetime import date

d1 = date.today()
d2 = d1         # Pas de copie : d1 et d2 pointe sur le même objet.

print("d1 = ", hex(id(d1)))
print("d2 = ", hex(id(d2)))

d3 = copy(d1)   # On copie la date dans un nouvel objet.
print("d3 = ", hex(id(d3)))
Duplication d'un objet via la fonction copy.

Appel au constructeur de la collection

Une autre solution consiste à utiliser le constructeur associé au type de la nouvelle instance souhaitée.

 1 
 2 
 3 
 4 
 5 
 6 
 7 
 8 
 9 
 10 
 11 
 12 
 13 
# Nous avons une première collection (une liste).
data = [10, 20, 30, 40, 50]

# Nous produisons une copie de data dans data2 en passant par le constructeur
# de la classe list : on obtient donc un nouvel objet.
data2 = list(data)

# Nous cherchons maintenant à modifier le contenu de data2.
data2[2] = 3000

# Et nous affichons les deux collections afin de valider le contenu de chacune.
print("data =", data)
print("data2 =", data2)
Duplication d'une liste via le constructeur de la classe list.
la notion de constructeur est liée à la Programmation Orientée Objet (POO). Si ces termes ne vous parlent pas trop pour le moment, ce n'est pas grave : nous allons y revenir dans de prochains chapitres de ce tutoriel.

En fait, cette technique est plus permissive, dans le sens ou l'on peut cloner les données d'une collection dans une autre collection de nature quelconque. L'exemple suivant duplique les données d'une liste vers un tuple et un set.

 1 
 2 
 3 
 4 
 5 
 6 
 7 
 8 
 9 
 10 
 11 
 12 
 13 
 14 
 15 
 16 
# Nous avons une première collection (une liste).
data = [10, 20, 30, 40, 50]

# On duplique les données de la liste dans un tuple
data2 = tuple(data)

# On fait de même vers un set
data3 = set(data)

# On modifie la collection d'origine
data.append(60)

# On affiche nos trois collections
print("data =", data)
print("data2 =", data2)
print("data3 =", data3)
Duplication d'une liste via le constructeur d'une classe d'une autre nature.

Et voici les résultats produits par cet exemple de code.

data = [10, 20, 30, 40, 50, 60]
data2 = (10, 20, 30, 40, 50)
data3 = {40, 10, 50, 20, 30}

Utilisation des list/set/dict en compréhension

Dans le cadre des collections, et ici d'une liste, on peut encore utiliser une autre technique pour cloner une collection : on peut utiliser une liste en compréhension (list comprehention, en anglais). Voici un exemple de code montrant cette possibilité.

 1 
 2 
 3 
 4 
 5 
 6 
 7 
 8 
 9 
 10 
 11 
 12 
 13 
# Nous avons une première collection (une liste).
data = [10, 20, 30, 40, 50]

# Nous produisons une copie de data dans data2 en passant par 
# une liste en compréhension
data2 = [value for value in data]

# Nous cherchons maintenant à modifier le contenu de data2.
data2[2] = 3000

# Et nous affichons les deux collections afin de valider le contenu de chacune.
print("data =", data)
print("data2 =", data2)
Duplication d'une liste via une nouvelle liste en compréhension

Bien entendu, les affichages sont toujours les mêmes, comme en atteste l'exemple d'exécution suivant.

data = [10, 20, 30, 40, 50]
data2 = [10, 20, 3000, 40, 50]

Qu'entend-on par copie peu profonde (Shallow Copy en anglais) ?

Arrivé à ce stade, vous devez-vous dire que vous êtes maintenant un « pro » de la duplication de collection : quelle erreur ! Les choses peuvent être bien plus subtiles que vous l'imaginez. Pour preuve, que pensez-vous de l'exemple de code suivant ?

 1 
 2 
 3 
 4 
 5 
 6 
 7 
 8 
 9 
 10 
 11 
# On crée une matrice et on la clone dans une autre variable.
matrix1 = [[10, 20, 30], [40, 50, 60], [70, 80, 90]]
matrix2 = list(matrix1)

# On modifie la liste clonée.
matrix2[1][1] = 5000
matrix2.append([100, 110, 120])

# On affiche nos deux matrices
print("matrix1 = ", matrix1)
print("matrix2 = ", matrix2)

Peut-être pensez-vous que la première collection restera inchangé : il en est rien ! Le problème c'est que nous avons à faire à une liste de listes. Dit autrement, dans le premier niveau de liste, on y stocke des pointeurs vers les listes du second niveau. Et ces pointeurs seront dupliqués dans matrix2. En conséquence, voici les résultats affichés par le programme précédent : notez bien le changement de la valeur 50 par 5000.

matrix1 =  [[10, 20, 30], [40, 5000, 60], [70, 80, 90]]
matrix2 =  [[10, 20, 30], [40, 5000, 60], [70, 80, 90], [100, 110, 120]]

Le diagramme suivant vous montre l'état de la mémoire au terme de l'exécution du programme précédent. Les données modifiées à partir de matrix2 sont affichées en rouge.

Le second niveau de liste n'a pas été dupliqué.

On parle de Shallow Copy (de copie peu profonde), car seule la première liste a été dupliquée. Et encore les choses sont simples, imaginez une matrice à trois dimensions (ou plus).

Si l'on reste sur l'exemple de notre matrice 3 x 3, sachez qu'avec les possibilités de clonage déjà présentées, nous pouvons dupliquer les deux niveaux de listes : il nous suffit de mixer une liste en compréhension pour dupliquer le premier niveau de liste, puis d'utiliser un constructeur (ou la méthode clone) pour le second niveau. Voici comment faire.

 1 
 2 
 3 
 4 
 5 
 6 
 7 
 8 
 9 
 10 
 11 
 12 
 13 
 14 
 15 
# On crée une matrice 3x3.
matrix1 = [[10, 20, 30], [40, 50, 60], [70, 80, 90]]

# On duplique, sur les deux niveaux de listes, matrix1
matrix2 = [list(line) for line in matrix1]
# On aurait aussi pu faire :
# matrix2 = [line.clone() for line in matrix1]

# On modifie la liste clonée.
matrix2[1][1] = 5000
matrix2.append([100, 110, 120])

# On affiche nos deux matrices
print("matrix1 = ", matrix1)
print("matrix2 = ", matrix2)
On duplique intégralement notre matrice 3 x 3

Cette fois-ci, les résultats constatés sont corrects.

matrix1 =  [[10, 20, 30], [40, 50, 60], [70, 80, 90]]
matrix2 =  [[10, 20, 30], [40, 5000, 60], [70, 80, 90], [100, 110, 120]]
si vous cherchez réellement à manipuler des matrices, au sens mathématique du terme, ne partez pas sur des listes de listes. Effectivement, cela ne serait pas très adapté, car la classe list ne propose aucune opération mathématique sérieuse. Dans ce cas, optez plutôt pour la librairie Numpy (il faut la télécharger) et elle propose des opérations d'algèbre linéaire (produit matriciel...) sur vecteurs et matrices.

Duplication d'une collection (de type Deep Copy)

Même si nous avons trouvé une solution pour cloner notre matrice, il nous faut des outils plus robustes pour gérer la duplication des objets en Python et ce quel que soit le niveau de profondeur constaté. On parle alors de Deep Copy (copie profonde).

Il aurait été bien d'avoir une opération de copie profonde nativement proposée sur n'importe quel objet Python : mais ce n'est pas le cas. A la place, nous allons charger et utiliser différents modules Python. Je vous propose deux alternatives.

Utilisation du module JSON

La première solution consiste à utiliser un moteur de sérialisation/désérialisation d'objets. La sérialisation consiste à transformer un objet complexe en un flux séquentiel d'octets. La désérialisation est l'opération inverse. Normalement, on utilise ces possibilités pour gérer la persistance de vos objets (la sauvegarde et rechargement d'objet à partir d'un fichier). Mais ici nous allons détourner le mécanisme pour reconstruire une copie de nos données.

Quand on parle de sérialisation et de désérialisation, il faut choisir le format de persistance. Je vous propose de choisir un format texte assez simple : le format JSON (JavaScript Object Notation). En fait, il est très proche de Python, mais c'est une autre histoire. Pour travailler en JSON il vous faudra charger le module json. Voici un exemple d'utilisation.

 1 
 2 
 3 
 4 
 5 
 6 
 7 
 8 
 9 
 10 
 11 
 12 
 13 
 14 
 15 
 16 
 17 
 18 
 19 
import json

# On crée une matrice.
matrix1 = [[10, 20, 30], [40, 50, 60], [70, 80, 90]]

# On utilise le module json pour cloner la matrice
json_content = json.dumps(matrix1)
print(type(json_content), json_content)     # Pour les curieux !
matrix2 = json.loads(json_content)
# On aurait aussi pu faire :
# matrix2 = json.loads(json.dumps(matrix1))

# On modifie la liste clonée.
matrix2[1][1] = 5000
matrix2.append([100, 110, 120])

# On affiche nos deux matrices
print("matrix1 = ", matrix1)
print("matrix2 = ", matrix2)
Duplication d'une liste de listes en passant par de la sérialisation JSON

Bien entendu, les résultats affichés restent les suivants.

<class 'str'> [[10, 20, 30], [40, 50, 60], [70, 80, 90]]
matrix1 =  [[10, 20, 30], [40, 50, 60], [70, 80, 90]]
matrix2 =  [[10, 20, 30], [40, 5000, 60], [70, 80, 90], [100, 110, 120]]

Utilisation du module copy

Deuxième solution : on utilise le module copy dont voici le contenu.

>>> import copy
>>> dir(copy)
['Error', '__all__', '__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', 
'__package__', '__spec__', '_copy_dispatch', '_copy_immutable', '_deepcopy_atomic', '_deepcopy_dict',
'_deepcopy_dispatch', '_deepcopy_list', '_deepcopy_method', '_deepcopy_tuple', '_keep_alive',
'_reconstruct', 'copy', 'deepcopy', 'dispatch_table', 'error']
>>> 

On y remarque principalement deux fonctions : copy qui réalise une copie peu profonde (Shallow copy) et deepcopy qui, comme son nom le laisse présager, réalise une copie profonde (Deep Copy). Voici un exemple d'utilisation de ce module.

 1 
 2 
 3 
 4 
 5 
 6 
 7 
 8 
 9 
 10 
 11 
 12 
 13 
 14 
 15 
from copy import deepcopy

# On crée une matrice.
matrix1 = [[10, 20, 30], [40, 50, 60], [70, 80, 90]]

# On utilise le module copy pour cloner la matrice
matrix2 = deepcopy(matrix1)

# On modifie la liste clonée.
matrix2[1][1] = 5000
matrix2.append([100, 110, 120])

# On affiche nos deux matrices
print("matrix1 = ", matrix1)
print("matrix2 = ", matrix2)
Utilisation de la fonction deepcopy

Encore une fois, les résultats affichés restent les suivants.

matrix1 =  [[10, 20, 30], [40, 50, 60], [70, 80, 90]]
matrix2 =  [[10, 20, 30], [40, 5000, 60], [70, 80, 90], [100, 110, 120]]

Travaux pratiques

Les énoncés

Exercice 1 : nous considérons le dictionnaire suivant permettant d'associer à des dates, des listes de tâches à effectuer.

 1 
 2 
 3 
 4 
 5 
tasks = {
    date(2022, 3, 26): ["Apprendre Python", "Manger", "Dormir"],
    date(2022, 3, 27): ["Poursuivre l'apprentissage de Python", "..."],
    date(2022, 3, 28): ["Se reposer un peu", "Dormir"]
}
Un dictionnaire contenant de données initiales.

Dupliquez proprement l'ensemble des taches à effectuer sans utiliser la fonction deepcopy. Une fois la copie réalisée, vous devez pouvoir la modifier sans altérer le dictionnaire initial.

Exercice 2 : refaire exactement la même chose, mais cette fois ci en utilisant la fonction deepcopy.

Comme toujours, essayez de faire ces exercices sans regarder directement la correction ci-dessous. ;-)

Les corrections

Exercice 1 : voici ma proposition de correction pour la première approche. J'ai choisi d'utiliser un dictionnaire en compréhension et le constructeur de la classe list.

 1 
 2 
 3 
 4 
 5 
 6 
 7 
 8 
 9 
 10 
 11 
 12 
 13 
 14 
 15 
 16 
 17 
 18 
 19 
 20 
 21 
 22 
 23 
 24 
 25 
 26 
from copy import deepcopy
from datetime import date
from pprint import PrettyPrinter

tasks = {
    date(2022, 3, 26): ["Apprendre Python", "Manger", "Dormir"],
    date(2022, 3, 27): ["Poursuivre l'apprentissage de Python", "..."],
    date(2022, 3, 28): ["Se reposer un peu", "Dormir"]
}


# On réalise la copie en utilisant un dictionnaire en compréhension et le constructeur de la classe list.
# On ne cherche pas à cloner les clés, car un objet date ne peut être altéré.
tasks_copy = { key: list(value) for key, value in tasks.items() }

# On produit quelques modifications sur les copies
tasks_copy[date(2022, 12, 31)] = ["Préparer le réveillon", "Réveillonner"]
tasks_copy[date(2022, 3, 27)].append("Encore...")

# On prépare un Pretty Printer pour afficher proprement les collections
pp = PrettyPrinter(indent=4, compact=False)

# On affiche les deux collections
pp.pprint(tasks)
print("---------------------------------------------------")
pp.pprint(tasks_copy)
Correction de l'exercice 1

Et voici le résultat produit par cet exemple.

{   datetime.date(2022, 3, 26): ['Apprendre Python', 'Manger', 'Dormir'],
    datetime.date(2022, 3, 27): ["Poursuivre l'apprentissage de Python", '...'],
    datetime.date(2022, 3, 28): ['Se reposer un peu', 'Dormir']}
---------------------------------------------------
{   datetime.date(2022, 3, 26): ['Apprendre Python', 'Manger', 'Dormir'],
    datetime.date(2022, 3, 27): [   "Poursuivre l'apprentissage de Python",
                                    '...',
                                    'Encore...'],
    datetime.date(2022, 3, 28): ['Se reposer un peu', 'Dormir'],
    datetime.date(2022, 12, 31): ['Préparer le réveillon', 'Réveillonner']}

Exercice 2 : et voici ma seconde proposition de correction en utilisant la fonction deepcopy.

 1 
 2 
 3 
 4 
 5 
 6 
 7 
 8 
 9 
 10 
 11 
 12 
 13 
 14 
 15 
 16 
 17 
 18 
 19 
 20 
 21 
 22 
 23 
 24 
 25 
from copy import deepcopy
from datetime import date
from pprint import PrettyPrinter

tasks = {
    date(2022, 3, 26): ["Apprendre Python", "Manger", "Dormir"],
    date(2022, 3, 27): ["Poursuivre l'apprentissage de Python", "..."],
    date(2022, 3, 28): ["Se reposer un peu", "Dormir"]
}


# On duplique la collection en utilisant la fonction deepcopy
tasks_copy = deepcopy(tasks)

# On produit quelques modifications sur les copies
tasks_copy[date(2022, 12, 31)] = ["Préparer le réveillon", "Réveillonner"]
tasks_copy[date(2022, 3, 27)].append("Encore...")

# On prépare un Pretty Printer pour afficher proprement les collections
pp = PrettyPrinter(indent=4, compact=False)

# On affiche les deux collections
pp.pprint(tasks)
print("---------------------------------------------------")
pp.pprint(tasks_copy)
Correction de l'exercice 1

Et voici le résultat produit par cet exemple.

{   datetime.date(2022, 3, 26): ['Apprendre Python', 'Manger', 'Dormir'],
    datetime.date(2022, 3, 27): ["Poursuivre l'apprentissage de Python", '...'],
    datetime.date(2022, 3, 28): ['Se reposer un peu', 'Dormir']}
---------------------------------------------------
{   datetime.date(2022, 3, 26): ['Apprendre Python', 'Manger', 'Dormir'],
    datetime.date(2022, 3, 27): [   "Poursuivre l'apprentissage de Python",
                                    '...',
                                    'Encore...'],
    datetime.date(2022, 3, 28): ['Se reposer un peu', 'Dormir'],
    datetime.date(2022, 12, 31): ['Préparer le réveillon', 'Réveillonner']}


Proprement afficher vos collections Bloc d'instructions et indentation