El día de hoy me gustaría hablemos acerca de Shallow y deep copy. Temas de suma importancia que debemos conocer siempre que nos encontremos trabajando con colecciones de objetos mutables (comúnmente listas) en Python.
Será un post sumamente interesante, así que te invito a que te quedes.
Bien, sin más introducción comencemos con el tema.
Variables
Antes de entrar de lleno en materia me gustaría aclarar un par de cosas. La primera, y quizás la más importante, es que en Python, a diferencia de otros lenguajes de programación, las variables debemos verlas como etiquetas que hacen referencia a un espacio en memoria, y no como cajas que almacenan algo.
Hago esta aclaración ya que es común que cuando comenzamos a programar se nos explique que las variables pueden ser vistas como pequeñas cajas donde almacenar algo. Si bien esa analogía puede funcionar para algunos lenguajes, la verdad es que en Python no es así.
Aquí un ejemplo.
user_one = 'Eduardo'
user_two = 'PyWombat'
>>> print(id(user_one))
4310802800
>>> print(id(user_two))
4310802736
Para este ejemplo he definido 2 variables con 2 valores completamente diferentes, y cómo podemos observar por sus ids son objetos diferentes.
Sin embargo, que paso si creo una tercera variable con un mismo valor.
user_one = 'Eduardo'
user_two = 'PyWombat'
user_three = 'Eduardo'
>>> print(id(user_one))
4310802800
>>> print(id(user_two))
4310802736
>>> print(id(user_three))
4310802800
Ahora con el uso de Ids podemos darnos cuenta que las variables user_one y user_two, que son variables diferentes, almacenan el mismo objeto.
Eso se debe a un concepto llamado Interning, del cual ya hablamos en una entrega anterior (Te comparto el link: ‘’).
Con este pequeño ejemplo podemos percatarnos que, un mismo objeto (en mi caso el String ‘Eduardo’) puede estar siendo referenciando por una N cantidad de variables. No necesariamente una variable significa un nuevo espacio en memoria.
Teniendo en cuenta esto, y que algunos objetos inmutables en Python son: Strings, Enteros, Flotantes, Booleanos y Tuplas, ya podemos comenzar con el tema principal.
Shallow and Deep Copy
Algo muy común cuando nos encontramos desarrollando Software es trabajar con listas de objetos mutables. Quizás listas de listas, listas de diccionarios o listas de objetos propios. En todos estos casos es sumamente importante discernir muy bien si nos encontramos trabajando con listas propias o listas que hagan referencia a otros objetos, esto con la finalidad de poder mantener una integridad en nuestros datos así evitar modificar datos de referencia.
Veamos un par de ejemplos. Imaginemos que tenemos una lista de usuarios.
user_one = { 'username': 'user1', 'email': 'user1@example.com' }
user_two = { 'username': 'user2', 'email': 'user2@example.com' }
users = [user_one, user_two]
Si quisiéramos realizar una copia de nuestra lista users tenemos un par de opciones para ello.
Recorrido de listas
new_usersw_list = users[:]
List Comprehention
new_users = [user for user in users]
Modulo copy
from copy import copy
new_users = copy(users)
Para estos 3 ejemplos estamos implementando un shallow copy, es decir, estamos creando una nueva variable con los elementos de la lista original. Con esta nueva variable ya podemos añadir nuevos usuarios, modificarlos o eliminarlos.
Lo que debemos tener muy presente es que, aunque estamos creando una nueva lista, esta hace referencia a objetos previamente definidos (user_one y user_two). Por lo tanto, si modificamos algún usuarios a partir de esta nueva lista, estos cambios se verán reflejado en nuestras variable.
new_users[0]['username'] = 'user0'
>>> print(user_one)
{'username': 'user0', 'email': 'user1@example.com'}
>>> print(users[0])
{'username': 'user0', 'email': 'user1@example.com'}
Esto también funciona a la inversa.
user_two['username'] = 'Cambio de username'
print(users[1])
{'username': 'Cambio de username', 'email': 'user2@example.com'}
print(new_users[1])
{'username': 'Cambio de username', 'email': 'user2@example.com'}
Cómo puedes observar estos cambios son se ven reflejado en todos aquellos lugares donde se hace referencia a los objetos.
Si bien el shallow copy es una muy buena idea cuando deseamos crear listas con objetos inmutables, no lo es tanto cuando trabajamos con listas de objetos mutables. Ya que como pudiste observar cualquier cambio que se realize a una referencia se verá reflejado en todas las demás variables. De nuevo, porque la variables no almacenan información, solo referencias.
Para evitar que la integridad de nuestros datos se vea comprometida, lo mejor que podemos hacer es trabajar con un deep copy (o copia profunda) donde es posible crear una nueva lista ya no con referencias, si no con nuevos objetos, objetos completamente independientes de la lista original.
Aquí el mismo ejemplo, pero con deep copy.
import copy
user_one = { 'username': 'user1', 'email': 'user1@example.com' }
user_two = { 'username': 'user2', 'email': 'user2@example.com' }
users = [user_one, user_two]
new_users = copy.deepcopy(users)
Ahora si intentamos modificar algún objeto de la nueva lista esto no debe repercutir en los objetos originales.
new_users[0]['username'] = 'user0'
print(user_one)
{'username': 'user1', 'email': 'user1@example.com'}
print(users[0])
{'username': 'user1', 'email': 'user1@example.com'}
print(new_users[0])
{'username': 'user0', 'email': 'user1@example.com'}
Cómo puedes observar, a la variable new_list tener objetos independientes de users cualquier cambio que realicemos no afectará de ninguna manera a otras referencias.
Inclusive podemos confirmar que son objetos diferentes usando la función id.
>>> print( id(new_users[0]) )
4310870080
>>> print( id(users[0]) )
4310788672
Letras finales.
Ahora ya lo sabes, siempre que te encuentres trabajando con listas con objetos mutables, debes tener presentes que las variables son referencias, cualquier cambio que hagas puede repercutir en todos los lugares donde se hagan uso.
Si bien el shallow copy es optimo en cuanto a memoria se refiere (Porque no se reservan nuevos espacio y solo se usan referencias que ya existen) también es importante mencionar que este abre una ventana de posibles errores al usarlo. Por lo tanto mi recomendación final es que, cuando te encuentre trabajando con listas mutables debes hacer uso de un deep copy, así podremos evitar cualquier problema de integridad de datos.