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
Cette vidéo vous explique comment cloner correctement une collection Python (list, tuple, set ou dictionnaire).
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 ! :-) |
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 :
Soit la ligne 4 duplique la collection et, dans ce cas, nous avons deux collections bien distinctes et l'on affichera [10, 20, 30, 40, 50]
pour data
.
Soit cette ligne duplique la variable, certes, mais cette variable contiendrait une référence (un pointeur) qui ciblerait uniquement l'adresse
mémoire de la collection. Dans ce cas la collection ne serait pas dupliquée et on afficherait donc aussi [10, 20, 3000, 40, 50]
pour
data
.
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.
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 >>>
data
et data2
.Mais alors, comment dupliquer une collection ?
En fait, il y a plusieurs manières de dupliquer une collection. Prenons le temps de les regarder une à une.
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'] >>>
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) |
Et voici les résultats produits par cet exemple.
data = [10, 20, 30, 40, 50] data2 = [10, 20, 3000, 40, 50]
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))) |
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) |
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) |
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}
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) |
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]
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.
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) |
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]]
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.
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.
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) |
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]]
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) |
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]]
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"] } |
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. ;-)
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) |
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) |
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']}
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 :