Les copies d’objets
Lorsqu’on souhaite faire des copies d’objets, il est important de comprendre les nuances entre les différents opérateurs pour éviter des erreurs courantes et potentiellement coûteuses. Parmi ces opérateurs, deux d’entre eux sont souvent confondus : l’opérateur d’affectation et l’opérateur de copie. Comprendre la distinction entre ces deux opérateurs permet de se prévenir des problèmes inattendus qui pourraient survenir lors de l’exécution du programme.
L’opérateur d’affectation est utilisé pour attribuer une valeur à une variable. Lorsqu’on utilise cet opérateur, on crée en réalité une nouvelle référence vers le même objet en mémoire. Ainsi, si l’on modifie l’objet référencé par l’une des variables, les autres variables qui pointent vers le même objet seront également affectées. Cela peut être utile dans certaines situations, mais cela peut aussi entraîner des comportements indésirables si l’on ne fait pas attention.
En revanche, l’opérateur de copie permet de créer un clone complet de l’objet en question. Autrement dit, il crée une nouvelle instance de l’objet avec les mêmes propriétés que l’objet original, mais sans partager la même référence en mémoire. Ainsi, les modifications apportées à l’un des objets n’affecteront pas l’autre. Cette approche est souvent préférée lorsque l’on souhaite travailler avec des objets indépendants et éviter les effets de bord indésirables.
L’opérateur d’affectation
L’opérateur d’affectation est sans doute l’un des opérateurs les plus couramment utilisés en programmation, mais il peut également être source de confusion pour les débutants en programmation orientée objet. Cette confusion provient souvent de la manière dont les objets sont manipulés et stockés en mémoire. Prenons l’exemple suivant : ma_voiture <- Voiture()
Dans ce cas, nous créons un nouvel objet de type Voiture
et nous affectons cet objet à la variable ma_voiture
. Toutefois, il est important de noter que la variable ma_voiture
ne contient pas l’objet Voiture
lui-même, mais plutôt une référence à cet objet. Autrement dit, ma_voiture
pointe vers l’emplacement mémoire où l’objet Voiture est stocké.
C’est précisément cette subtilité qui peut être source d’erreurs pour les programmeurs débutants. En effet, la variable ne contient pas directement l’objet, mais une référence vers celui-ci. Ainsi, lorsqu’on réaffecte une seconde fois la variable, on agit en réalité sur la référence et non sur l’objet lui-même. Ainsi, lorsqu’on utilise l’opérateur d’affectation pour attribuer une variable à une autre, comme dans l’exemple une_voiture <- ma_voiture
, il est important de comprendre ce qui se passe réellement en arrière-plan. À première vue, on pourrait penser qu’une copie de la variable est créée, mais ce n’est pas le cas. Ce qu’il se passe en réalité, c’est que les deux variables pointent désormais vers la même référence d’objet en mémoire.
Dans cet exemple, une_voiture
et ma_voiture
font toutes deux référence au même objet Voiture
. Ainsi, si l’on modifie l’objet Voiture
via l’une des variables, l’autre variable sera également affectée, puisqu’elles partagent la même référence. Il est donc important de bien comprendre que l’opérateur d’affectation ne crée pas une nouvelle instance de l’objet, mais qu’il réaffecte simplement la référence à cet objet.
L’opérateur de copie
L’opérateur de copie peut prendre différentes formes selon le langage de programmation utilisé. Certains langages exigent l’implémentation d’une interface spécifique, tandis que d’autres nécessitent une surcharge d’opérateur ou l’utilisation d’un constructeur par copie. Malgré ces différences, l’objectif principal de cet opérateur reste le même : créer un clone complet d’un objet.
Par exemple, pour la copie d’une moto : moto1 <- Moto()
. Ici, nous créons une instance de la classe Moto et l’assignons à la variable moto1
. Si nous souhaitons créer une copie indépendante de cette instance, nous devons aider le langage de programmation à comprendre comment effectuer cette copie. Cependant, l’opérateur par copie standard peut ne pas donner les résultats escomptés, car il n’est programmé que pour les types primitif qu’il connait.
Lorsque vous faites la copie d’un objet qui contient d’autres objets, les langages de programmation feront une copie des références internes. C’est-à-dire que si notre moto possède elle-même des références vers des objets Roue()
, le comportement par défaut de la copie d’une moto ne copiera pas les roues! Nous aurons donc bien deux motos distingue avec deux châssis différents, mais qui utiliseront les mêmes roues…
Le comportement par défaut de l’opérateur de copie nous donne donc ceci :
Pour remédier à ce problème, vous devez explicitement définir la manière dont l’opérateur de copie doit fonctionner pour notre classe Moto
. Cela implique de spécifier comment les attributs de l’objet doivent être copiés, ainsi que de gérer les éventuelles références à d’autres objets. En fournissant ces informations, nous permettons au langage de programmation de créer une copie conforme de notre objet, sans partager les mêmes références en mémoire, et ainsi éviter les effets de bord indésirables.
Pour réécrire cet opérateur de copie, vous devez généralement rajouter une fonctionnalité spécifique à votre classe :
import copy class Moto: def __init__(self, marque, modele, couleur): self.marque = marque self.modele = modele self.couleur = couleur def __copy__(self): return Moto(self.marque, self.modele, self.couleur) def __deepcopy__(self, memo): return Moto(copy.deepcopy(self.marque, memo), copy.deepcopy(self.modele, memo), copy.deepcopy(self.couleur, memo))
class Moto { public: std::string marque; std::string modele; std::string couleur; Moto(const std::string& marque, const std::string& modele, const std::string& couleur) : marque(marque), modele(modele), couleur(couleur) {} Moto(const Moto& other) : marque(other.marque), modele(other.modele), couleur(other.couleur) {} };
public class Moto { public string marque; public string modele; public string couleur; public Moto(string marque, string modele, string couleur) { this.marque = marque; this.modele = modele; this.couleur = couleur; } public Moto(Moto other) { marque = other.marque; modele = other.modele; couleur = other.couleur; } }
Après quoi, la copie complète, aussi appelée deepcopy, peut être réalisée :