Module `copy` de Python

Module `copy` de Python

Cet article explique le module copy de Python.

En nous concentrant sur les différences entre les copies superficielles et profondes, nous proposons une explication claire — des mécanismes de base de la duplication d’objets jusqu’aux applications dans des classes personnalisées — accompagnée d’exemples pratiques.

YouTube Video

Module copy de Python

Le module copy de Python est le module standard pour gérer la duplication (copie) d’objets. L’objectif est de comprendre la différence entre les copies superficielles et profondes et de pouvoir contrôler le comportement de copie pour des objets personnalisés.

Bases : qu’est-ce qu’une copie superficielle ?

Ici, nous illustrons le comportement des copies superficielles pour les objets mutables tels que les listes et les dictionnaires. Une copie superficielle ne duplique que l’objet de niveau supérieur et partage les références internes.

 1# Example: shallow copy with lists and dicts
 2import copy
 3
 4# A nested list
 5original = [1, [2, 3], 4]
 6shallow = copy.copy(original)
 7
 8# Mutate nested element
 9shallow[1].append(99)
10
11print("original:", original)  # nested change visible in original
12print("shallow: ", shallow)

Dans ce code, la liste de niveau supérieur est dupliquée, mais les sous-listes internes sont partagées par référence, de sorte que shallow[1].append(99) se répercute également dans original. C’est un comportement typique d’une copie superficielle.

Qu’est-ce qu’une copie profonde ?

Une copie profonde duplique récursivement l’objet et tous les objets qu’il référence en interne, produisant un objet indépendant. À utiliser lorsque vous souhaitez dupliquer en toute sécurité des structures imbriquées complexes.

 1# Example: deep copy with nested structures
 2import copy
 3
 4original = [1, [2, 3], {'a': [4, 5]}]
 5deep = copy.deepcopy(original)
 6
 7# Mutate nested element of the deep copy
 8deep[1].append(100)
 9deep[2]['a'].append(200)
10
11print("original:", original)  # original remains unchanged
12print("deep:    ", deep)

Dans cet exemple, les modifications apportées à deep n’affectent pas original. deepcopy copie l’ensemble du graphe d’objets, produisant une réplique indépendante.

Comment décider laquelle utiliser

Le choix du type de copie doit être déterminé par la structure de l’objet et votre objectif. Si vous ne voulez pas que les modifications des éléments internes mutables affectent l’objet d’origine, deepcopy est approprié. C’est parce qu’elle duplique récursivement l’ensemble de l’objet, créant une copie totalement indépendante.

En revanche, si dupliquer uniquement l’objet de niveau supérieur suffit et que vous privilégiez la vitesse et l’efficacité mémoire, copy (copie superficielle) est plus approprié.

  • Vous voulez éviter que l’original change lorsque vous modifiez des éléments internes mutables → utilisez deepcopy.
  • Dupliquer uniquement le niveau supérieur suffit et vous voulez privilégier les performances (vitesse/mémoire) → utilisez copy (copie superficielle).

Gestion des objets intégrés et immuables

Les objets immuables (p. ex., int, str et les tuples dont le contenu est immuable) n’ont généralement pas besoin d’être copiés. copy.copy peut renvoyer le même objet lorsque c’est approprié.

 1# Example: copying immutables
 2import copy
 3
 4a = 42
 5b = copy.copy(a)
 6print(a is b)  # True for small ints (implementation detail)
 7
 8s = "hello"
 9t = copy.deepcopy(s)
10print(s is t)  # True (strings are immutable)

Comme copier des objets immuables apporte peu de bénéfice en termes d’efficacité, le même objet peut être réutilisé. Vous avez rarement à vous en soucier dans la conception d’applications.

Classes personnalisées : définir __copy__ et __deepcopy__

Si la copie par défaut ne correspond pas à ce que vous attendez pour votre classe, vous pouvez fournir votre propre logique de copie. Si vous définissez __copy__ et __deepcopy__, copy.copy et copy.deepcopy les utiliseront.

 1# Example: customizing copy behavior
 2import copy
 3
 4class Node:
 5    def __init__(self, value, children=None):
 6        self.value = value
 7        self.children = children or []
 8
 9    def __repr__(self):
10        return f"Node({self.value!r}, children={self.children!r})"
11
12    def __copy__(self):
13        # Shallow copy: create new Node, but reuse children list reference
14        new = self.__class__(self.value, self.children)
15        return new
16
17    def __deepcopy__(self, memo):
18        # Deep copy: copy value and deepcopy each child
19        new_children = [copy.deepcopy(child, memo) for child in self.children]
20        new = self.__class__(copy.deepcopy(self.value, memo), new_children)
21        memo[id(self)] = new
22        return new
  • En implémentant __copy__ et __deepcopy__, vous pouvez contrôler de manière flexible le comportement de copie propre à la classe. Par exemple, vous pouvez gérer des cas où certains attributs doivent faire référence au même objet (le partager), tandis que d'autres attributs doivent être dupliqués en tant qu'objets entièrement nouveaux.
  • L'argument memo passé à __deepcopy__ est un dictionnaire qui enregistre les objets déjà traités lors de la copie récursive et empêche les boucles infinies dues aux références circulaires.
 1# Build tree
 2root_node = Node('root', [Node('child1'), Node('child2')])
 3
 4shallow_copy = copy.copy(root_node)   # shallow copy
 5deep_copy = copy.deepcopy(root_node)  # deep copy
 6
 7# Modify children
 8# affects both root_node and shallow_copy
 9shallow_copy.children.append(Node('child_shallow'))
10# affects deep_copy only
11deep_copy.children.append(Node('child_deep'))
12
13# Print results
14print("root_node:", root_node)
15print("shallow_copy:", shallow_copy)
16print("deep_copy:", deep_copy)
  • Dans ce code, la variable shallow_copy est créée via une 'copie superficielle', de sorte que seul le niveau supérieur de l'objet est dupliqué, et les objets auxquels il fait référence en interne (dans ce cas, la liste children) sont partagés avec l'original root_node. Par conséquent, lorsque vous ajoutez un nouveau nœud à shallow_copy, la liste children partagée est mise à jour et la modification se répercute également sur le contenu de root_node.
  • En revanche, la variable deep_copy est créée via une 'copie profonde', de sorte que la structure interne est dupliquée récursivement, produisant un arbre complètement indépendant de root_node. Par conséquent, même si vous ajoutez un nouveau nœud à deep_copy, cela n'affecte pas root_node.

Références cycliques (objets récursifs) et importance de memo

Lorsqu’un graphe d’objets complexe se référence lui‑même (références cycliques), deepcopy utilise un dictionnaire memo pour suivre les objets déjà copiés et éviter les boucles infinies.

 1# Example: recursive list and deepcopy memo demonstration
 2import copy
 3
 4a = []
 5b = [a]
 6a.append(b)  # a -> [b], b -> [a]  (cycle)
 7
 8# deepcopy can handle cycles
 9deep = copy.deepcopy(a)
10print("deep copy succeeded, length:", len(deep))
  • En interne, deepcopy utilise memo pour référencer les objets déjà dupliqués, ce qui permet une copie sûre même en présence de cycles. Vous avez besoin d’un mécanisme similaire lorsque vous effectuez la récursion manuellement.

copyreg et une copie personnalisée de type sérialisation (avancé)

Si vous souhaitez enregistrer un comportement de copie spécial en coordination avec des bibliothèques, vous pouvez utiliser le module standard copyreg pour enregistrer la façon dont les objets sont (re)construits. C’est utile pour les objets complexes et les types d’extensions C.

 1# Example: using copyreg to customize pickling/copy behavior (brief example)
 2import copy
 3import copyreg
 4
 5class Wrapper:
 6    def __init__(self, data):
 7        self.data = data
 8
 9def reduce_wrapper(obj):
10    # Return callable and args so object can be reconstructed
11    return (Wrapper, (obj.data,))
12
13copyreg.pickle(Wrapper, reduce_wrapper)
14
15w = Wrapper([1, 2, 3])
16w_copy = copy.deepcopy(w)
17print("w_copy.data:", w_copy.data)
  • En utilisant copyreg, vous pouvez fournir des règles de reconstruction qui affectent à la fois pickle et deepcopy. C’est une API avancée, et dans la plupart des cas __deepcopy__ suffit.

Mises en garde et pièges pratiques

Il existe plusieurs points importants pour garantir un comportement correct lors de l’utilisation du module copy. Nous expliquons ci‑dessous les pièges courants que vous pouvez rencontrer en développement et comment les éviter.

  • Performances deepcopy peut consommer beaucoup de mémoire et de CPU ; réfléchissez donc soigneusement à sa nécessité.
  • Gestion des éléments que vous souhaitez partager Si vous voulez que certains attributs, comme de gros caches, restent partagés, évitez intentionnellement de copier leurs références dans __deepcopy__.
  • État interne immuable Si vous conservez des données immuables en interne, la copie peut être inutile.
  • Threads et ressources externes Les ressources qui ne peuvent pas être copiées, comme les sockets et les descripteurs de fichier, sont soit dénuées de sens à copier, soit sources d’erreurs ; il faut donc éviter de les copier par conception.

Exemple pratique : un modèle pour mettre à jour des dictionnaires en toute sécurité

Lors de la mise à jour d’un objet de configuration complexe, cet exemple utilise deepcopy pour protéger les paramètres d’origine.

 1# Example: safely update a nested configuration using deepcopy
 2import copy
 3
 4default_config = {
 5    "db": {"host": "localhost", "ports": [5432]},
 6    "features": {"use_cache": True, "cache_sizes": [128, 256]},
 7}
 8
 9# Create a working copy to modify without touching defaults
10working = copy.deepcopy(default_config)
11working["db"]["ports"].append(5433)
12working["features"]["cache_sizes"][0] = 512
13
14print("default_config:", default_config)
15print("working:", working)

En utilisant deepcopy, vous pouvez créer en toute sécurité des configurations dérivées sans risquer d’altérer les paramètres par défaut. C’est particulièrement utile pour les structures mutables imbriquées, telles que les configurations.

Bonnes pratiques

Pour utiliser le module copy en toute sécurité et efficacement, il est important de garder à l'esprit les recommandations pratiques suivantes.

  • Choisissez entre copie superficielle et copie profonde en fonction de la probabilité de modifications et de leur portée.
  • Implémentez __copy__ et __deepcopy__ dans les classes personnalisées pour obtenir le comportement attendu.
  • Les copies profondes étant coûteuses, réduisez le besoin de copie par la conception lorsque c’est possible. Envisagez l’immuabilité et des méthodes de clonage explicites, entre autres techniques.
  • Lorsqu’il s’agit de références cycliques, utilisez deepcopy ou fournissez manuellement un mécanisme similaire à memo.
  • Concevez de sorte que les ressources externes, telles que les descripteurs de fichiers et les threads, ne soient pas copiées.

Conclusion

Le module copy est l’outil standard pour la duplication d’objets en Python. En comprenant correctement les différences entre copies superficielles et profondes et en implémentant un comportement de copie personnalisé lorsque c’est nécessaire, vous pouvez dupliquer de manière sûre et prévisible. En précisant dès la phase de conception si la copie est réellement nécessaire et ce qui doit être partagé, vous pouvez éviter des bogues et des problèmes de performances inutiles.

Vous pouvez suivre l'article ci-dessus avec Visual Studio Code sur notre chaîne YouTube. Veuillez également consulter la chaîne YouTube.

YouTube Video