Assignment Statements Do Not Copy Objects

You might already know that python assignment statements do not copy objects. Instead, they create bindings between a target and an object. When you assign a variable to another variable, you are not creating a copy of the object. Instead, you are creating a new reference to the same object.

a = [1, 2, 3]
b = a

print(a is b)
# True

Now this might seem like a copy, but it’s not. It’s just two names for the same object. If you change one, you change the other.

a = [1, 2, 3]
b = a

a.append(4)
print(b)

# [1, 2, 3, 4]

You might think why this could be a problem. Well, it’s not a problem if you know what you’re doing. But if you’re not careful, you might end up with unexpected results. Let’s look at an example:

We use dictionaries to store some of the configuration values. A class/object might be a better option to store the configs in this case. But for the sake of this example, let’s use dictionaries.


import matplotlib.pyplot as plt

# Default plot configuration settings
default_plot_config = {
    'xlabel': 'X-axis',
    'ylabel': 'Y-axis',
    'title': 'Default Title',
    'color': 'blue',
    'linestyle': '-',
    'linewidth': 1.0,
}

# Create a custom plot configuration by copying the default (shallow copy)
custom_plot_config1 = default_plot_config

# Modify the custom plot configuration
custom_plot_config1['title'] = 'Custom Plot 1'
custom_plot_config1['color'] = 'red'

# Create another custom plot configuration by deep copying the default
custom_plot_config2 = default_plot_config

# Modify the second custom plot configuration
custom_plot_config2['title'] = 'Custom Plot 2'
custom_plot_config2['linewidth'] = 2.0

# Sample data
x = [1, 2, 3, 4, 5]
y = [2, 4, 6, 8, 10]

# Create plots using custom configurations
plt.figure(figsize=(8, 6))  # Set the figure size

plt.subplot(2, 1, 1)
plt.plot(x, y, label='Data', color=custom_plot_config1['color'], linestyle=custom_plot_config1['linestyle'], linewidth=custom_plot_config1['linewidth'])
plt.xlabel(custom_plot_config1['xlabel'])
plt.ylabel(custom_plot_config1['ylabel'])
plt.title(custom_plot_config1['title'])
plt.legend()

plt.subplot(2, 1, 2)
plt.plot(x, y, label='Data', color=custom_plot_config2['color'], linestyle=custom_plot_config2['linestyle'], linewidth=custom_plot_config2['linewidth'])
plt.xlabel(custom_plot_config2['xlabel'])
plt.ylabel(custom_plot_config2['ylabel'])
plt.title(custom_plot_config2['title'])
plt.legend()

plt.tight_layout()  # Adjust subplot spacing

plt.show()

The above code produces the following output:

Simple Plot

As you can see, both plots have the same title, color, and line style. This is because when we assigned the default plot configuration to the custom plot configurations, we didn’t create copies. Instead, we created references to the same object. So when we modified the custom plot configurations, we modified the default plot configuration.

Shallow Copy

A shallow copy creates a new object and then inserts references to the objects found in the original. In essence, a shallow copy is only one level deep. The copying process does not recurse and therefore won’t create copies of the child objects themselves. But in some cases, this might be exactly what you want. Let’s look at an example:

import copy

# Default plot configuration settings
default_plot_config = {
    'xlabel': 'X-axis',
    'ylabel': 'Y-axis',
    'title': 'Default Title',
    'color': 'blue',
    'linestyle': '-',
    'linewidth': 1.0,
}

# Create a custom plot configuration by copying the default (shallow copy)
custom_plot_config1 = copy.copy(default_plot_config)

# Modify the custom plot configuration
custom_plot_config1['title'] = 'Custom Plot 1'
custom_plot_config1['color'] = 'red'

Now we are creating a shallow copy of the default plot configuration. This means that the custom plot configuration will have its own copy of the default plot configuration. But the child objects will still be references to the original objects.

Deep Copy

A deep copy creates a new object and then recursively inserts copies into it of the objects found in the original. In essence everything is copied recursively, resulting in a fully independent clone of the original object and all of its children. Let’s look at an example:

import copy

# Default plot configuration settings
default_plot_config = {
    'xlabel': 'X-axis',
    'ylabel': 'Y-axis',
    'title': 'Default Title',
    'color': 'blue',
    'linestyle': '-',
    'linewidth': 1.0,
}

# Create another custom plot configuration by deep copying the default
custom_plot_config2 = copy.deepcopy(default_plot_config)

# Modify the second custom plot configuration
custom_plot_config2['title'] = 'Custom Plot 2'
custom_plot_config2['linewidth'] = 2.0

Fixing the above code.

The complete code using the copy module is as follows:

import copy
import matplotlib.pyplot as plt

# Default plot configuration settings
default_plot_config = {
    'xlabel': 'X-axis',
    'ylabel': 'Y-axis',
    'title': 'Default Title',
    'color': 'blue',
    'linestyle': '-',
    'linewidth': 1.0,
}

# Create a custom plot configuration by copying the default (shallow copy)
custom_plot_config1 = copy.copy(default_plot_config)

# Modify the custom plot configuration
custom_plot_config1['title'] = 'Custom Plot 1'
custom_plot_config1['color'] = 'red'

# Create another custom plot configuration by copying the default (shallow copy)
custom_plot_config2 = copy.copy(default_plot_config)

# Modify the second custom plot configuration
custom_plot_config2['title'] = 'Custom Plot 2'
custom_plot_config2['linewidth'] = 2.0

# Sample data
x = [1, 2, 3, 4, 5]
y = [2, 4, 6, 8, 10]

# Create plots using custom configurations
plt.figure(figsize=(8, 6))  # Set the figure size

plt.subplot(2, 1, 1)

plt.plot(x, y, label='Data', color=custom_plot_config1['color'], linestyle=custom_plot_config1['linestyle'], linewidth=custom_plot_config1['linewidth'])

plt.xlabel(custom_plot_config1['xlabel'])
plt.ylabel(custom_plot_config1['ylabel'])
plt.title(custom_plot_config1['title'])
plt.legend()

plt.subplot(2, 1, 2)
plt.plot(x, y, label='Data', color=custom_plot_config2['color'], linestyle=custom_plot_config2['linestyle'], linewidth=custom_plot_config2['linewidth'])
plt.xlabel(custom_plot_config2['xlabel'])
plt.ylabel(custom_plot_config2['ylabel'])
plt.title(custom_plot_config2['title'])
plt.legend()

plt.tight_layout()  # Adjust subplot spacing

plt.show()

This will produce the following output:

Simple Plot 2 using copy()

As you can see, the plots now have different titles, colors, and line styles. This is because we created a copy of the default plot configuration. So when we modified the custom plot configurations, we didn’t modify the default plot configuration.

Shallow copies of dict and list objects

The python dictionary method has its own copy() method that returns a shallow copy of the dictionary.

d = {'a': 1, 'b': 2}
d2 = d.copy()

Similarly, a list object in python can be copied by using the slice operator [:].

l = [1, 2, 3]
l2 = l[:]

Some problems with deepcopy()

As mentioned earlier, deepcopy() creates a copy of the object and all of its children. deepcopy() is only relevant for compound objects (objects that contain other objects, like lists or class instances).

Problem 1: Recursive Object Complex objects that have references to themselves, either directly or indirectly, can result in endless recursive loops.

Problem 2: Over-Copying with Deep Copy Deep copy, as it replicates all elements, can sometimes lead to over-copying, including data that should be shared among the copies.

For Example,

import copy

default_config = {'theme': 'light', 'font_size': 12}
config1 = copy.deepcopy(default_config)
config2 = copy.deepcopy(default_config)

# Modify config1
config1['theme'] = 'dark'

print(default_config)  # {'theme': 'light', 'font_size': 12}
print(config1)         # {'theme': 'dark', 'font_size': 12}
print(config2)         # {'theme': 'light', 'font_size': 12}

In the above example, when we used deepcopy(), we inadvertently create copies of the entire dictionary, including the parts meant to be shared (‘font_size’). Modifying one configuration (config1) doesn’t affect the others, but it consumes more memory than necessary. In such cases, a shallow copy or a custom copy mechanism might be more appropriate.

Conclusion

In this article, we explored the difference between shallow copy and deep copy in Python. Also discussed some examples of how to use the copy module to create shallow and deep copies of objects in Python. Along with that, we learned about some of the potential problems with deepcopy() and how to avoid them. Hope you enjoyed reading this article. Happy coding! 🚀

References