Python deep copy object
Let's commence by examining how to duplicate the default collections in Python. By invoking their factory methods on an existing collection, the built-in mutable collections provided by Python, such as lists, dicts, and sets, can be copied:
new_list = list(original_list)
new_dict = dict(original_dict)
new_set = set(original_set)
However, this approach only generates shallow duplicates and is ineffective for bespoke objects. There is a significant distinction between shallow and deep cloning for complex objects like lists, dicts, and sets:
Creating a new collection object and then populating it with references to the child objects discovered in the original is known as a shallow duplicate. A shallow duplicate is essentially one step deep. Since the copying procedure does not recurse, duplicates of the offspring items themselves will not be produced.
The duplicating procedure is iterative when using a deep duplicate. It entails first building a fresh collection object, which is then iteratively filled with duplicates of the initial collection object's children.
By copying an object in this manner, the initial object and all of its children are completely independent clones.
Making Insufficient Prints
The following example uses the list() factory function to build a new nested list and then shallowly duplicate it:
>>> xs = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
>>> ys = list(xs) # Make a shallow copy
With the same elements as xs, ys will now be a brand-new, independent entity. You can confirm this by looking at the two items:
>>> xs
[[1, 2, 3], [4, 5, 6], [7, 8, 9]]
>>> ys
[[1, 2, 3], [4, 5, 6], [7, 8, 9]]
Let's design a small exercise to show that ys is truly distinct from the original. Try modifying the original (xs) by adding a new sublist, then verify that the change did not impact the duplicate (ys):
>>> xs.append(['new sublist'])
>>> xs
[[1, 2, 3], [4, 5, 6], [7, 8, 9], ['new sublist']]
>>> ys
[[1, 2, 3], [4, 5, 6], [7, 8, 9]]
You can see that it had the desired outcome. It was simple to make "superficial" changes to the duplicated list.
But ys still has references to the original child items kept in xs because we only made a partial duplicate of the original collection.
These kids weren't clones. They were simply repeated in the duplicated list as references.
Because both lists contain the same child objects, if you change one of the child objects in list xs, this change will also be mirrored in list ys. The copy is only one level deep and shallow:
>>> xs[1][0] = 'X'
>>> xs
[[1, 2, 3], ['X', 5, 6], [7, 8, 9], ['new sublist']]
>>> ys
[[1, 2, 3], ['X', 5, 6], [7, 8, 9]]
The only modification we (apparently) made to xs in the case above. However, it comes out that the number 1 sublists in both xs and ys were changed. Again, this occurred as a result of our incomplete copy of the initial roster.
Both items would have been completely independent had the first stage involved making a deep duplicate of xs. The real distinction between shallow and deep duplicates of an item is as follows.
Now that you are aware of shallow and deep cloning, you can make shallow duplicates of some of the built-in collection classes. The inquiries that remain unanswered are:
How are built-in groups made into deep copies?
How can clones (both shallow and deep) of any kind of object, including special types, be made?
The clone function in the Python standard library contains the answers to these queries. A straightforward interface is provided by this module for making shallow and deep clones of any Python object.
Create Deep Versions
Let's use the prior list-copying example once more, but with a crucial modification. Instead, we'll make a deep duplicate this time by using the deepcopy() method found in the copy module:
>>> import copy
>>> xs = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
>>> zs = copy.deepcopy(xs)
You can see that xs and zs, the copies we made using copy.deepcopy(), are identical when you examine them, just like in the prior example:
>>> xs
[[1, 2, 3], [4, 5, 6], [7, 8, 9]]
>>> zs
[[1, 2, 3], [4, 5, 6], [7, 8, 9]]
However, you can see that if you change one of the child objects in the initial object (xs), the deep duplicate won't be affected (zs).
This time, the original and the duplicate are equally autonomous. Recursively cloning xs produced copies of all its offspring objects:
>>> xs[1][0] = 'X'
>>> xs
[[1, 2, 3], ['X', 5, 6], [7, 8, 9]]
>>> zs
[[1, 2, 3], [4, 5, 6], [7, 8, 9]]
At this point, you might want to settle down with the Python interpreter and explore through these examples. It is simpler to comprehend copying things when you can interact with and experiment with real-world instances.
By the way, a method in the duplicate module allows you to make shallow copies as well. Shallow duplicates of objects are made by the copy.copy() method.
This is helpful if you need to make it obvious in your code that you're making a shallow duplicate of something.
Copy.copy() can be used to convey this information. However, it's regarded as more Pythonic to merely use the list, dict, and set factory methods to make shallow duplicates for built-in collections.
Python Cloning Any Python Classes
How do we make clones (shallow and deep) of any objects, including unique classes, is an issue that remains unanswered. Let's look at that right away.
Once more, the duplicate function saves the day. Any object can be duplicated using its copy.copy() and copy.deepcopy() methods.
Once more, a straightforward exercise is the best method to comprehend how to use these. I'll model this on the list-copying illustration from earlier. Let's begin by outlining a straightforward 2D point class:
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})'
I think it's safe to say that this was fairly simple. So that we can quickly examine objects made from this class in the Python interpreter, I added a version of __repr__().
Note: The string given by __repr__ in the sample above was created using a Python 3.6 f-string. You would use a separate string formatting formula on Python 2 and Python 3 versions prior to 3.6, for example:
def __repr__(self):
return 'Point(%r, %r)' % (self.x, self.y)
Next, we'll make a Point instance and use the copy module to (shallowly) duplicate it:
>>> a = Point(23, 42)
>>> b = copy.copy(a)
If we look at the (shallow) clone and the original Point object's elements, we find what we would anticipate to see:
>>> a
Point(23, 42)
>>> b
Point(23, 42)
>>> a is b
False
Here is another factor to bear in mind. Because the values of our point object are fixed types (ints), there is no distinction between a shallow and deep duplicate in this scenario. But I'll elaborate on the case later.
Let's move on to a case that is more intricate. In order to symbolize 2D rectangles, I'm going to create another class. I'll do it so that we can build a more intricate object structure by using Point objects to symbolize the coordinates of my rectangles:
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})')
Once more, we'll try to make a shallow duplicate of a rectangular first:
rect = Rectangle(Point(0, 1), Point(5, 6))
srect = copy.copy(rect)
You can see how well the __repr__() override is functioning and that the shallow copy process performed as anticipated by closely examining the original rectangle and its duplicate:
>>> rect
Rectangle(Point(0, 1), Point(5, 6))
>>> srect
Rectangle(Point(0, 1), Point(5, 6))
>>> rect is srect
False
Do you still recall how the prior list example demonstrated the distinction between deep and superficial copies?
The same strategy will be applied here. I'll make changes to an object further down the object chain, and the (shallow) duplicate will also update to reflect those changes:
>>> rect.topleft.x = 999
>>> rect
Rectangle(Point(999, 1), Point(5, 6))
>>> srect
Rectangle(Point(999, 1), Point(5, 6))
I trust this performed as you anticipated. The initial square will then be deeply copied. I'll then make another change, and you can see which items are impacted by it here:
>>> 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))
This time, the shallow duplicate (shrect) and deep copy (drect) are completely autonomous of one another (srect).
Even though we've covered a lot of territory, there are some subtleties to copying things.
You might want to read up on the clone module instructions because it pays to dig deep (ha!) into this subject.
For instance, by specifying the special methods __copy__() and __deepcopy__() on them, objects can regulate how they are duplicated.