Aller au contenu
> 💻 🧠 Code 1001 > 📑 Fiches Mémo > 🐍 Fiches Mémo Python > Copie d’objets en Python

Copie d’objets en Python

🔎 Pourquoi la copie d’objets est-elle si importante ?

En Python, les variables ne sont pas des « boîtes » contenant des données, mais plutôt des « étiquettes » ou des références pointant vers des objets en mémoire. Lorsque vous effectuez une simple affectation, comme celle-ci :

liste_a = [1, 2, 3]
liste_b = liste_a

Vous ne créez pas une nouvelle liste. Vous créez simplement une autre référence (liste_b) qui pointe vers le même objet liste.

👉 Toute modification de liste_a sera immédiatement répercutée dans liste_b (et vice versa) car il s’agit du même objet.

Analogie : Imaginez que vous avez un document Google Docs. Si vous envoyez un lien à un ami, il modifiera votre document original. C’est une affectation. En revanche, si vous sélectionnez « Fichier » → « Créer une copie », un document complètement indépendant est créé. Le module copy en Python existe pour créer ce genre de copies indépendantes.

⚙️ Deux types de copie

Le module copy fournit deux fonctions clés :

  • copy.copy() — crée une copie superficielle (shallow copy).
  • copy.deepcopy() — crée une copie profonde (deep copy).

La principale différence entre elles réside dans la manière dont elles traitent les objets imbriqués (comme des listes à l’intérieur de listes ou des dictionnaires à l’intérieur de dictionnaires).


🟦 Copie superficielle (copy.copy())

copy.copy() crée un nouvel objet conteneur de haut niveau, mais au lieu de copier les objets imbriqués, il y insère des références vers les objets originaux.

import copy

liste_originale = [1, 2, [3, 4]]
copie_superficielle = copy.copy(liste_originale)

# Modifier la liste imbriquée dans l'original
liste_originale[2][0] = 99

print(f"Original : {liste_originale}")    # Affiche : Original : [1, 2, [99, 4]]
print(f"Copie superficielle : {copie_superficielle}") # Affiche : Copie superficielle : [1, 2, [99, 4]]

Comme vous pouvez le voir, la modification de la liste imbriquée a affecté à la fois l’original et la copie, car ils partagent cette liste imbriquée.

Diagramme ASCII :

liste_originale     → [ 1, 2, ───► liste_interne ]
copie_superficielle → [ 1, 2, ───► liste_interne ]
                                 liste_interne   → [ 99, 4 ]

🟩 Copie profonde (copy.deepcopy())

copy.deepcopy() résout le problème des références partagées. Cette fonction parcourt récursivement l’ensemble de l’objet et crée des copies complètes et indépendantes de tout ce qu’elle rencontre, y compris tous les objets imbriqués.

import copy

liste_originale = [1, 2, [3, 4]]
copie_profonde = copy.deepcopy(liste_originale)

# Modifier la liste imbriquée dans l'original
liste_originale[2][0] = 99

print(f"Original : {liste_originale}") # Affiche : Original : [1, 2, [99, 4]]
print(f"Copie profonde : {copie_profonde}")     # Affiche : Copie profonde : [1, 2, [3, 4]]

Désormais, la copie_profonde est complètement indépendante.

Diagramme ASCII :

liste_originale → [ 1, 2, ───► liste_a ]
copie_profonde  → [ 1, 2, ───► liste_b ]
                            liste_a   → [ 99, 4 ]
                            liste_b   → [ 3, 4 ]

📑 Aide-mémoire : copy.copy() vs copy.deepcopy()

Type d’objetExemplecopy.copy() (superficielle)copy.deepcopy() (profonde)
list (liste)[1, 2, [3, 4]]Une nouvelle liste, mais la liste imbriquée [3, 4] est partagéeUne copie entièrement indépendante ; la liste imbriquée est aussi copiée
dict (dictionnaire){"a": 1, "b": {"c": 2}}Un nouveau dictionnaire, mais le {"c": 2} imbriqué est partagéUn dictionnaire indépendant et tous les dictionnaires imbriqués
set (ensemble){1, 2, (3, 4)}Un nouvel ensemble ; les éléments mutables imbriqués seraient partagésUn nouvel ensemble ; tous les éléments imbriqués sont copiés récursivement
tuple(1, 2, [3, 4])Retourne le même tuple, mais la liste imbriquée est partagéeUn nouveau tuple avec une copie de la liste imbriquée
str, int, float, bool"salut", 42, 3.14, TrueRetourne le même objet (ils sont immuables, pas besoin de copier)Identique à la copie superficielle
classe personnaliséeNoeud(1, Noeud(2))Un nouvel objet de haut niveau ; les objets imbriqués sont partagésUne copie entièrement indépendante de l’objet et de tous les objets imbriqués
objet avec __slots__Une classe avec __slots__Seul l’objet de haut niveau est copiéTous les objets imbriqués sont copiés s’ils le permettent
fichier / socket / fluxopen("file.txt")Erreur (objet non copiable)Erreur (objet non copiable)

🧪 Exemples concrets pour le tableau

📌 list

import copy
a = [1, 2, [3, 4]]
b = copy.copy(a)
c = copy.deepcopy(a)
a[2][0] = 99
print(f"copy: {b}")      # [1, 2, [99, 4]]
print(f"deepcopy: {c}")  # [1, 2, [3, 4]]

📌 dict

import copy
a = {"a": 1, "b": {"c": 2}}
b = copy.copy(a)
c = copy.deepcopy(a)
a["b"]["c"] = 42
print(f"copy: {b}")      # {'a': 1, 'b': {'c': 42}}
print(f"deepcopy: {c}")  # {'a': 1, 'b': {'c': 2}}

📌 set

import copy
a = {1, 2, (3, 4)} # Les ensembles ne peuvent contenir que des types immuables
b = copy.copy(a)
c = copy.deepcopy(a)
print(b == c) # True (mais ce sont deux objets indépendants en mémoire)

📌 tuple

import copy
a = (1, 2, [3, 4]) # Le tuple est immuable, mais son élément de type liste ne l'est pas
b = copy.copy(a)
c = copy.deepcopy(a)
a[2][0] = 99
print(f"copy: {b}")      # (1, 2, [99, 4])
print(f"deepcopy: {c}")  # (1, 2, [3, 4])

📌 Classe personnalisée

import copy
class Noeud:
    def __init__(self, valeur, enfant=None):
        self.valeur = valeur
        self.enfant = enfant

a = Noeud(1, Noeud(2))
b = copy.copy(a)
c = copy.deepcopy(a)
a.enfant.valeur = 99
print(f"copy: {b.enfant.valeur}")      # 99
print(f"deepcopy: {c.enfant.valeur}")  # 2

📌 Personnalisation du comportement : __copy__ et __deepcopy__

Vous pouvez contrôler la manière dont vos propres objets sont copiés en définissant des méthodes spéciales dans leur classe.

import copy
class Personnalisee:
    def __init__(self, x):
        self.x = x
    def __copy__(self):
        print("Appel de __copy__ !")
        # Retourne une nouvelle instance, mais avec la même référence aux données internes
        return Personnalisee(self.x)
    def __deepcopy__(self, memo):
        # memo est un dictionnaire pour suivre les objets déjà copiés (évite la récursion)
        print("Appel de __deepcopy__ !")
        # Crée une copie entièrement nouvelle, y compris les données imbriquées
        return Personnalisee(copy.deepcopy(self.x, memo))

obj = Personnalisee([1, 2])
c1 = copy.copy(obj)       # Affiche : Appel de __copy__ !
c2 = copy.deepcopy(obj)   # Affiche : Appel de __deepcopy__ !

⚡ Performances et pièges

  • Vitesse : copy.copy() est beaucoup plus rapide car il ne parcourt pas récursivement toute la structure de données. copy.deepcopy() peut être très lent pour des objets volumineux et profondément imbriqués.
  • Objets non copiables : Les ressources système comme les fichiers ouverts, les sockets ou les flux ne peuvent pas être copiées. Tenter de le faire lèvera une erreur.
  • Références cycliques : deepcopy est assez intelligent pour gérer les références cycliques (lorsqu’un objet A fait référence à B, et B à son tour à A) sans entrer dans une boucle infinie.

Quand utiliser quoi : Les règles principales

  1. Utilisez une copie superficielle (copy.copy()) si votre objet ne contient que des données immuables ou si vous souhaitez intentionnellement que la copie et l’original partagent des objets imbriqués. C’est rapide et efficace.
  2. Utilisez une copie profonde (copy.deepcopy()) lorsque vous avez besoin d’une copie complète et 100% indépendante de l’original, en particulier lorsque vous travaillez avec des structures de données complexes (listes, dictionnaires, instances de classe).
  3. Pour les objets immuables (nombres, chaînes de caractères, tuples sans éléments mutables), la copie est généralement inutile — Python optimise déjà leur utilisation.
  4. N’oubliez pas que vous pouvez personnaliser le processus avec __copy__ et __deepcopy__ pour un contrôle total.

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *