En Python, el manejo de objetos y referencias es un concepto fundamental. Es crucial comprender las diferencias entre la asignación de variables, la copia superficial (shallow copy) y la copia profunda (deep copy) para evitar comportamientos inesperados en el código, especialmente al menipular estructuras de datos complejas o mutables. Este artículo explora estas distinciones clave a través de ejemplos prácticos.
Asignación de Objetos
Cuando se asigna una variable a otra en Python, no se crea un nuevo objeto en memoria; ambas variables simplemente apuntan a la misma ubicación de memoria que contiene el objeto original. Esto significa que cualquier modificación realizada a través de una de las variables será reflejada automáticametne al acceder al objeto a través de la otra.
datos_originales = ["Alice", 30, ["Java", "SQL"]]
referencia_copia = datos_originales
print(f"ID de datos_originales: {id(datos_originales)}")
print(f"Contenido de datos_originales: {datos_originales}")
print(f"IDs de elementos de datos_originales: {[id(e) for e in datos_originales]}")
print(f"\nID de referencia_copia: {id(referencia_copia)}")
print(f"Contenido de referencia_copia: {referencia_copia}")
print(f"IDs de elementos de referencia_copia: {[id(e) for e in referencia_copia]}")
# Modificando el objeto a través de datos_originales
datos_originales[0] = "Alicia"
datos_originales[2].append("C++")
print(f"\nDespués de modificar datos_originales:")
print(f"Contenido de datos_originales: {datos_originales}")
print(f"IDs de elementos de datos_originales: {[id(e) for e in datos_originales]}")
print(f"Contenido de referencia_copia: {referencia_copia}")
print(f"IDs de elementos de referencia_copia: {[id(e) for e in referencia_copia]}")
Como se observa en el ejemplo, datos_originales y referencia_copia comparten el mismo ID de objeto y los IDs de sus elementos. Al modificar un elemento de la lista (por ejemplo, cambiando el nombre o añadiendo una habilidad a la lista anidada), los cambios se reflejan en ambas "variables" porque en realidad están interactuando con el mismo objeto subyacente. Los tipos inmutables como las cadenas (str) se reemplazan, creando una nueva cadena en esa posición, pero la lista contenedora sigue siendo el mismo objeto. Para objetos mutables anidados, la modificación (como append) ocurre directamente en el objeto compartido.
Copia Superficial (Shallow Copy)
Una copia superficial crea un nuevo objeto compuesto (como una lista o un diccionario) y luego inserta en él las referencias a los objetos encontrados en el original. Esto significa que el nuevo objeto es una entidad distinta del original, pero sus elementos internos (especialmente los objetos mutables anidados) siguen apuntando a los mismos objetos que el original.
import copy
datos_principales = ["Bob", 25, ["Python", "Flask"]]
copia_superficial = copy.copy(datos_principales)
print(f"ID de datos_principales: {id(datos_principales)}")
print(f"Contenido de datos_principales: {datos_principales}")
print(f"IDs de elementos de datos_principales: {[id(elem) for elem in datos_principales]}")
print(f"\nID de copia_superficial: {id(copia_superficial)}")
print(f"Contenido de copia_superficial: {copia_superficial}")
print(f"IDs de elementos de copia_superficial: {[id(elem) for elem in copia_superficial]}")
# Modificando el original
datos_principales[0] = "Roberto"
datos_principales[2].append("Django")
print(f"\nDespués de modificar datos_principales:")
print(f"Contenido de datos_principales: {datos_principales}")
print(f"IDs de elementos de datos_principales: {[id(elem) for elem in datos_principales]}")
print(f"Contenido de copia_superficial: {copia_superficial}")
print(f"IDs de elementos de copia_superficial: {[id(elem) for elem in copia_superficial]}")
Como se puede observar, copia_superficial tiene un ID de objeto diferente a datos_principales, indicando que son dos listas distintas. Sin embargo, los IDs de los elementos anidados (como la lista de habilidades) son idénticos. Esto implica que si se modifica un elemento mutable dentro de la lista original (por ejemplo, añadiendo "Django" a la lista de habilidades), ese cambio se reflejará tanto en el original como en la copia superficial. Si un elemento imutable (como el nombre "Bob") se reemplaza en el original, la copia superficial mantiene su referencia al objeto inmutable original, ya que las referencias externas se comportan como en una asignación.
Otros métodos comunes para realizar copias superficiales incluyen el uso de la sintaxis de rebanado ([:]) para listas, el constructor de tipo (ej. list(), dict()) o el método .copy() específico de algunos tipos de colecciones.
Copia Profunda (Deep Copy)
Una copia profunda, realizada con copy.deepcopy(), crea un nuevo objeto compuesto y, recursivamente, inserta copias de todos los objetos encontrados en el original. Esto significa que no solo el objeto principal es una entidad nueva y separada, sino que también todos los objetos anidados (incluyendo los mutables) son copias nuevas y distintas, completamente independientes del original.
import copy
inventario_maestro = ["Laptop", 1200, ["RAM 16GB", "SSD 512GB"]]
duplicado_completo = copy.deepcopy(inventario_maestro)
print(f"ID de inventario_maestro: {id(inventario_maestro)}")
print(f"Contenido de inventario_maestro: {inventario_maestro}")
print(f"IDs de elementos de inventario_maestro: {[id(item) for item in inventario_maestro]}")
print(f"\nID de duplicado_completo: {id(duplicado_completo)}")
print(f"Contenido de duplicado_completo: {duplicado_completo}")
print(f"IDs de elementos de duplicado_completo: {[id(item) for item in duplicado_completo]}")
# Modificando el original
inventario_maestro[0] = "Ordenador"
inventario_maestro[2].append("GPU RTX3070")
print(f"\nDespués de modificar inventario_maestro:")
print(f"Contenido de inventario_maestro: {inventario_maestro}")
print(f"IDs de elementos de inventario_maestro: {[id(item) for item in inventario_maestro]}")
print(f"Contenido de duplicado_completo: {duplicado_completo}")
print(f"IDs de elementos de duplicado_completo: {[id(item) for item in duplicado_completo]}")
En este caso, duplicado_completo no solo es un objeto diferente de inventario_maestro (diferentes IDs principales), sino que sus elementos anidados (como la lista de especificaciones) también tienen IDs distintos. La modificación de la lista de especificaciones dentro de inventario_maestro no tiene ningún efecto sobre duplicado_completo, ya que ambas listas anidadas son objetos completamente independientes en memoria.
Consideraciones Adicionales sobre Copias
Es importante considerar algunas particularidades al trabajar con copias en Python:
- **Tipos Inmutables:** Para objetos inmutables como números, cadenas (
str) o tuplas que contienen solo tipos inmutables, el concepto de copia profunda o superficial no aplica de la misma manera. Dado que no se pueden modificar, una "copia" generalmente resultará en una referencia al mismo objeto o a un nuevo objeto idéntico que se considera igual debido a la inmutabilidad (ej.1 is copy.copy(1)o'abc' is copy.deepcopy('abc')). En esencia, no hay datos subyacentes que "copiar" o "modificar" de forma independiente. - **Tuplas con Objetos Mutables:** Si una tupla contiene referencias a objetos mutables (ej. una lista dentro de una tupla),
copy.deepcopy()sí creará nuevas copias de esos objetos mutables anidados, a diferencia decopy.copy(), que solo copiaría las referencias a esos objetos mutables. La tupla en sí será un nuevo objeto.