Copiar un objeto en Python es tan simple como usar "=", ¿verdad?
Hacer una copia en Python usando el operador = no está mal, pero es posible que como resultado obtengas un comportamiento inesperado, que no se ajusta realmente al que estás esperando. Esto sucede porque el = no crea un nuevo objeto, sino solo una variable que se refiere al objeto original. Suena más complejo de lo que es, de modo que para entenderla realmente, exploremos esta idea en mayor profundidad.
Digamos que tenemos una lista de números (mis_numeros), y tenemos la intención de copiarla en una nueva lista (copia_numeros), para lo cual utilizaremos el símbolo =. Veamos qué sucede, si luego de "copiarla", agregamos un nuevo elemento a la lista original.
Consideremos el siguiente ejemplo:
mis_numeros = [5, 10, 20]
copia_numeros = mis_numeros
mis_numeros.append(40)
print(mis_numeros)
>> [5, 10, 20, 40]
print(copia_numeros)
>> [5, 10, 20, 40]
Como podemos ver, agregar el número 40 a la lista mis_numeros, también lo ha agregado a la lista copia_numeros, es decir, a la lista que habíamos creado utilizando el operador =. Si con nuestro método append solo hicimos referencia a la variable mis_numeros, ¿cómo puede haber ocurrido esto?
Las variables en Python no almacenan directamente valores u objetos, sino referencias a ellos. Por lo tanto, cuando se realiza una asignación, no se copian esos valores, sino que se está creando un nuevo nombre (o una nueva referencia) para el mismo objeto creado. En otras palabras, el operador = ha creado una copia superficial. En una copia superficial, el objeto copiado depende del objeto original. En una copia superficial, solamente se copian las referencias a los elementos contenidos en el objeto.
La mayoría de las veces queremos crear una copia completamente independiente. Esto se conoce como una copia profunda. Para asegurarnos de ello, podemos importar la librería copy, que nos proporciona un método llamado deepcopy (copia profunda), el cual precisamente nos ayuda a alcanzar este objetivo.
import copy
mis_numeros = [5, 10, 20]
copia_numeros = copy.deepcopy(mis_numeros)
mis_numeros.append(40)
print(mis_numeros)
>> [5, 10, 20, 40]
print(copia_numeros)
>> [5, 10, 20]
Vemos que ahora, los objetos se han mantenido "independientes", permitiéndonos realizar operaciones sobre la copia sin preocuparnos por modificar el objeto original. Esta es una consideración que siempre vale la pena tener en mente al programar en Python.
Para profundizar aún más, podemos verificar que en Python el concepto de "copia superficial" y "copia profunda" se aplica para los tipos de datos mutables (listas, sets, diccionarios), mientras que para los objetos inmutables (stings, tuplas, números), simplemente existen asignaciones.
Podemos auxiliarnos en cualquier momento de la función id(), la cual devuelve el número identificador que asigna Python a un objeto, algo que podríamos comparar con la "ubicación en memoria" (sin que sea esto en modo alguno una localización real del objeto).
Si aplicamos el método id() sobre nuestro ejemplo inicial:
mis_numeros = [5, 10, 20]
copia_numeros = mis_numeros
print(id(mis_numeros))
print(id(copia_numeros))
Podremos comprobar que en ambos casos se devuelve el mismo número (ejecuta el código anterior en tu editor de código preferido), lo que para nosotros significa que es en realidad el mismo objeto. Si realizamos alguna operación:
mis_numeros = [5, 10, 20]
copia_numeros = mis_numeros
mis_numeros.append(40)
print(id(mis_numeros))
print(id(copia_numeros))
Podremos verificar que aún así es el mismo objeto, lo que soporta con un nuevo dato, el comportamiento que habíamos observado (la lista modificada tiene dos nombres por los cuales podemos acceder a ella: mis_numeros y copia_numeros).
Como hemos hablado hasta el momento siempre de objetos mutables, tomémonos el último párrafo para replicar el análisis sobre un tipo de objeto inmutable, como por ejemplo, un número entero:
mi_numero = 3
otro_numero = mi_numero
print(id(mi_numero))
print(id(otro_numero))
En ambos casos, se imprimirá el mismo valor, dando cuenta que es el mismo objeto del que hablamos. Sin embargo:
mi_numero = 3
otro_numero = mi_numero
mi_numero += 1
print(id(mi_numero))
print(id(otro_numero))
Veremos que el mi_numero y otro_numero tienen números de identificación diferentes, diferentes valores entre sí, y que por lo tanto se han modificado independientemente uno del otro. Recuerda la distinción: el tipo de dato es inmutable, pero la variable puede contener y asignársele cualquier dato. Como un objeto inmutable no tiene el potencial de (valga la redundancia), mutar, puede identificarse siempre en memoria con igual identificación. Dos tipos de datos inmutables de diferente valor, tendrán, necesariamente, números de identificación diferentes. Dos objetos inmutables iguales tienen el mismo número de identificación, pero dos variables con objetos mutables, aún tengan los mismos valores, solo tendrán el mismo número de identificación si una es una "copia superficial" de la otra, y en cuyo caso, mutarán en consonancia.
Artículo complementario: ¿Cómo se manipula e intercambia información en Python?