Copie peu profonde ou profonde d’objets Python

Copie peu profonde ou profonde d'objets Python

Assignment statements in Python do not create copies d'objets, ils ne lient les noms qu'à un objet. Pour les objets immuables, cela ne fait généralement pas de différence.

Mais pour travailler avec des objets mutables ou des collections d'objets mutables, vous cherchez peut-être un moyen de créer de «vraies copies» ou «clones» de ces objets.

Essentiellement, vous voudrez parfois des copies que vous pouvez modifierwithout en modifiant automatiquement l'original en même temps. Dans cet article, je vais vous donner un aperçu de la façon de copier ou de «cloner» des objets dans Python 3 et certaines des mises en garde impliquées.

Note: Ce tutoriel a été écrit avec Python 3 à l'esprit mais il y a peu de différence entre Python 2 et 3 quand il s'agit de copier des objets. Lorsqu'il y a des différences, je les signalerai dans le texte.

Commençons par voir comment copier les collections intégrées de Python. Les collections mutables intégrées de Python commelists, dicts, and sets peuvent être copiées en appelant leurs fonctions d'usine sur une collection existante:

new_list = list(original_list)
new_dict = dict(original_dict)
new_set = set(original_set)

Cependant, cette méthode ne fonctionnera pas pour les objets personnalisés et, en plus de cela, elle ne crée que desshallow copies. Pour les objets composés tels que les listes, les dictionnaires et les ensembles, il existe une différence importante entre la copieshallow etdeep:

  • Unshallow copy signifie construire un nouvel objet de collection, puis le remplir avec des références aux objets enfants trouvés dans l'original. En substance, une copie superficielle n'est queone level deep. Le processus de copie n'est pas récurrent et ne crée donc pas de copies des objets enfants eux-mêmes.

  • Undeep copy rend le processus de copie récursif. Cela signifie d'abord construire un nouvel objet de collection, puis le remplir récursivement avec des copies des objets enfants trouvés dans l'original. La copie d'un objet de cette façon parcourt l'arborescence d'objets entière pour créer un clone entièrement indépendant de l'objet d'origine et de tous ses enfants.

Je sais, c'était un peu bouchée. Voyons donc quelques exemples pour faire ressortir cette différence entre les copies profondes et peu profondes.

Free Bonus:Click here to get access to a chapter from Python Tricks: The Book qui vous montre les meilleures pratiques de Python avec des exemples simples que vous pouvez appliquer instantanément pour écrire du code + Pythonic plus beau.

Faire des copies superficielles

Dans l'exemple ci-dessous, nous allons créer une nouvelle liste imbriquée, puisshallowly la copier avec la fonction d'usine delist():

Cela signifie queys sera désormais un nouvel objet indépendant avec le même contenu quexs. Vous pouvez le vérifier en inspectant les deux objets:

Pour confirmer queys est vraiment indépendant de l'original, imaginons une petite expérience. Vous pouvez essayer d'ajouter une nouvelle sous-liste à l'original (xs), puis vérifier que cette modification n'affecte pas la copie (ys):

>>>

Comme vous pouvez le voir, cela a eu l'effet escompté. La modification de la liste copiée à un niveau «superficiel» n'a posé aucun problème.

Cependant, comme nous n'avons créé qu'une copieshallow de la liste d'origine,ys contient toujours des références aux objets enfants d'origine stockés dansxs.

Ces enfants ont été copiésnot. Ils ont simplement été référencés à nouveau dans la liste copiée.

Par conséquent, lorsque vous modifiez l'un des objets enfants dansxs, cette modification sera également reflétée dansys - c'est parce queboth lists share the same child objects. La copie n'est qu'une copie superficielle superficielle d'un niveau:

>>>

>>> xs[1][0] = 'X'
>>> xs
[[X', 5, 6], [7, 8, 9], ['new sublist']]
>>> ys
[[X', 5, 6], [7, 8, 9]]

Dans l'exemple ci-dessus, nous n'avons (apparemment) modifié quexs. Mais il s'avère que les sous-listesboth à l'index 1 enxsandys ont été modifiées. Encore une fois, cela s'est produit parce que nous n'avions créé qu'une copieshallow de la liste d'origine.

Si nous avions créé une copiedeep dexs dans la première étape, les deux objets auraient été totalement indépendants. C'est la différence pratique entre les copies d'objets peu profondes et profondes.

Vous savez maintenant comment créer des copies superficielles de certaines des classes de collection intégrées, et vous savez la différence entre la copie superficielle et la copie profonde. Les questions pour lesquelles nous voulons toujours des réponses sont:

  • Comment pouvez-vous créer des copies complètes des collections intégrées?

  • Comment pouvez-vous créer des copies (superficielles et profondes) d'objets arbitraires, y compris des classes personnalisées?

La réponse à ces questions se trouve dans le modulecopy de la bibliothèque standard Python. Ce module fournit une interface simple pour créer des copies superficielles et profondes d'objets Python arbitraires.

Faire des copies profondes

Répétons l'exemple de copie de liste précédent, mais avec une différence importante. Cette fois, nous allons créer une copie dedeep en utilisant la fonctiondeepcopy() définie dans le modulecopy à la place:

>>>

>>> import copy
>>> xs = [[zs = copy.deepcopy(xs)

Lorsque vous inspectezxs et son clonezs que nous avons créé aveccopy.deepcopy(), vous verrez qu'ils sont à nouveau identiques, tout comme dans l'exemple précédent:

>>>

Cependant, si vous apportez une modification à l'un des objets enfants de l'objet d'origine (xs), vous verrez que cette modification n'affectera pas la copie complète (zs).

Les deux objets, l'original et la copie, sont totalement indépendants cette fois. xs a été cloné de manière récursive, y compris tous ses objets enfants:

>>>

Vous voudrez peut-être prendre le temps de vous asseoir avec l'interpréteur Python et de lire ces exemples dès maintenant. Envelopper votre tête autour de la copie d'objets est plus facile lorsque vous apprenez et jouez avec les exemples de première main.

En passant, vous pouvez également créer des copies superficielles en utilisant une fonction du modulecopy. La fonctioncopy.copy() crée des copies superficielles des objets.

Ceci est utile si vous devez clairement communiquer que vous créez une copie superficielle quelque part dans votre code. L'utilisation decopy.copy() vous permet d'indiquer ce fait. Cependant, pour les collections intégrées, il est considéré plus Pythonic d'utiliser simplement les fonctions list, dict et set d'usine pour créer des copies superficielles.

Copie d'objets Python arbitraires

La question à laquelle nous devons encore répondre est de savoir comment créer des copies (superficielles et profondes) d'objets arbitraires, y compris des classes personnalisées. Voyons cela maintenant.

Encore une fois, le modulecopy vient à notre secours. Ses fonctionscopy.copy() etcopy.deepcopy() peuvent être utilisées pour dupliquer n'importe quel objet.

Encore une fois, la meilleure façon de comprendre comment les utiliser est avec une simple expérience. Je vais baser cela sur l'exemple de copie de liste précédent. Commençons par définir une classe de points 2D simple:

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __repr__(self):
        return f'Point({self.x!r}, {self.y!r})'

J'espère que vous conviendrez que c'était assez simple. J'ai ajouté une implémentation de__repr__() afin que nous puissions facilement inspecter les objets créés à partir de cette classe dans l'interpréteur Python.

Note: L'exemple ci-dessus utilise unPython 3.6 f-string pour construire la chaîne retournée par__repr__. Sur Python 2 et les versions de Python 3 antérieures à 3.6, vous utiliseriez une expression de formatage de chaîne différente, par exemple:

def __repr__(self):
    return 'Point(%r, %r)' % (self.x, self.y)

Ensuite, nous allons créer une instancePoint, puis la copier (superficiellement) en utilisant le modulecopy:

>>>

>>> a = Point(23, 42)
>>> b = copy.copy(a)

Si nous inspectons le contenu de l’objetPoint original et de son clone (peu profond), nous voyons ce à quoi nous nous attendions:

>>>

>>> a
Point(23, 42)
>>> b
Point(23, 42)
>>> a is b
False

Voici autre chose à garder à l'esprit. Étant donné que notre objet ponctuel utilise des types immuables (ints) pour ses coordonnées, il n'y a pas de différence entre une copie superficielle et une copie profonde dans ce cas. Mais je développerai l'exemple dans une seconde.

Passons à un exemple plus complexe. Je vais définir une autre classe pour représenter des rectangles 2D. Je vais le faire d'une manière qui nous permet de créer une hiérarchie d'objets plus complexe - mes rectangles utiliseront les objetsPoint pour représenter leurs coordonnées:

class Rectangle:
    def __init__(self, topleft, bottomright):
        self.topleft = topleft
        self.bottomright = bottomright

    def __repr__(self):
        return (f'Rectangle({self.topleft!r}, '
                f'{self.bottomright!r})')

Encore une fois, nous allons d'abord essayer de créer une copie superficielle d'une instance de rectangle:

rect = Rectangle(Point(0, 1), Point(5, 6))
srect = copy.copy(rect)

Si vous inspectez le rectangle d'origine et sa copie, vous verrez à quel point le remplacement de__repr__() fonctionne bien et que le processus de copie superficielle a fonctionné comme prévu:

>>>

>>> rect
Rectangle(Point(0, 1), Point(5, 6))
>>> srect
Rectangle(Point(0, 1), Point(5, 6))
>>> rect is srect
False

Rappelez-vous comment l'exemple de liste précédent illustre la différence entre les copies profondes et peu profondes? Je vais utiliser la même approche ici. Je vais modifier un objet plus profondément dans la hiérarchie des objets, puis vous verrez ce changement reflété dans la copie (peu profonde) également:

>>>

>>> rect.topleft.x = 999
>>> rect
Rectangle(Point(999, 1), Point(5, 6))
>>> srect
Rectangle(Point(999, 1), Point(5, 6))

J'espère que cela s'est comporté comme vous vous y attendiez. Ensuite, je vais créer une copie complète du rectangle d'origine. Ensuite, j'appliquerai une autre modification et vous verrez quels objets sont affectés:

>>>

>>> drect = copy.deepcopy(srect)
>>> drect.topleft.x = 222
>>> drect
Rectangle(Point(222, 1), Point(5, 6))
>>> rect
Rectangle(Point(999, 1), Point(5, 6))
>>> srect
Rectangle(Point(999, 1), Point(5, 6))

Voila! Cette fois, la copie complète (drect) est totalement indépendante de l'original (rect) et de la copie superficielle (srect).

Nous avons parcouru beaucoup de terrain ici, et il y a encore des points plus fins à copier des objets.

Cela vaut la peine d'aller en profondeur (ha!) Sur ce sujet, vous voudrez peut-être étudier lescopy module documentation. Par exemple, les objets peuvent contrôler la manière dont ils sont copiés en définissant les méthodes spéciales__copy__() et__deepcopy__() sur eux.

3 choses à retenir

  • Faire une copie superficielle d'un objet ne clone pas les objets enfants. Par conséquent, la copie n'est pas totalement indépendante de l'original.

  • Une copie complète d'un objet clonera récursivement des objets enfants. Le clone est entièrement indépendant de l'original, mais la création d'une copie complète est plus lente.

  • Vous pouvez copier des objets arbitraires (y compris des classes personnalisées) avec le modulecopy.

Si vous souhaitez approfondir d'autres techniques de programmation Python de niveau intermédiaire, découvrez ce bonus gratuit:

Free Bonus:Click here to get access to a chapter from Python Tricks: The Book qui vous montre les meilleures pratiques de Python avec des exemples simples que vous pouvez appliquer instantanément pour écrire du code + Pythonic plus beau.