La copia de objetos es un concepto fundamental en Python, y su incomprensión a menudo conduce a errores difíciles de encontrar. Cuando modificas un objeto y otro cambia inesperadamente, el problema probablemente radica en cómo fue copiado. Aclaremos esto de una vez por todas.
🔎 Para empezar, ¿por qué es importante la copia de objetos?
En Python, las variables no son «cajas» que contienen datos, sino «etiquetas» o referencias que apuntan a objetos en la memoria. Cuando realizas una asignación simple, como esta:
lista_a = [1, 2, 3]
lista_b = lista_a
No estás creando una nueva lista. Simplemente estás creando otra referencia (lista_b) que apunta exactamente al mismo objeto de lista.
👉 Cualquier cambio en lista_a se reflejará inmediatamente en lista_b (y viceversa) porque son el mismo objeto.
Analogía: Imagina que tienes un documento de Google Docs. Si le envías un enlace a un amigo, él estará editando tu documento original. Eso es una asignación. Sin embargo, si seleccionas «Archivo» → «Hacer una copia», se crea un documento completamente independiente. El módulo copy en Python existe para crear este tipo de copias independientes.
⚙️ Dos tipos de copia
El módulo copy proporciona dos funciones clave:
copy.copy()— crea una copia superficial (shallow copy).copy.deepcopy()— crea una copia profunda (deep copy).
La principal diferencia entre ellas es cómo manejan los objetos anidados (como listas dentro de listas o diccionarios dentro de diccionarios).
🟦 Copia superficial (copy.copy())
copy.copy() crea un nuevo objeto contenedor de nivel superior pero, en lugar de copiar los objetos anidados, inserta referencias a ellos en el nuevo contenedor.
import copy
lista_original = [1, 2, [3, 4]]
copia_superficial = copy.copy(lista_original)
# Modifica la lista anidada en el original
lista_original[2][0] = 99
print(f"Original: {lista_original}") # Salida: Original: [1, 2, [99, 4]]
print(f"Copia superficial: {copia_superficial}") # Salida: Copia superficial: [1, 2, [99, 4]]
Como puedes ver, modificar la lista anidada afectó tanto al original como a la copia porque comparten esa lista anidada.
Diagrama ASCII:
lista_original → [ 1, 2, ───► lista_interna ]
copia_superficial → [ 1, 2, ───► lista_interna ]
lista_interna → [ 99, 4 ]
🟩 Copia profunda (copy.deepcopy())
copy.deepcopy() resuelve el problema de las referencias compartidas. Esta función recorre recursivamente todo el objeto y crea copias completas e independientes de todo lo que encuentra, incluyendo todos los objetos anidados.
import copy
lista_original = [1, 2, [3, 4]]
copia_profunda = copy.deepcopy(lista_original)
# Modifica la lista anidada en el original
lista_original[2][0] = 99
print(f"Original: {lista_original}") # Salida: Original: [1, 2, [99, 4]]
print(f"Copia profunda: {copia_profunda}") # Salida: Copia profunda: [1, 2, [3, 4]]
Ahora, la copia_profunda es completamente independiente.
Diagrama ASCII:
lista_original → [ 1, 2, ───► lista_a ]
copia_profunda → [ 1, 2, ───► lista_b ]
lista_a → [ 99, 4 ]
lista_b → [ 3, 4 ]
📑 Tabla de referencia: copy.copy() vs copy.deepcopy()
| Tipo de objeto | Ejemplo | copy.copy() (superficial) | copy.deepcopy() (profunda) |
|---|---|---|---|
| list (lista) | [1, 2, [3, 4]] | Una nueva lista, pero la lista anidada [3, 4] es compartida | Una copia completamente independiente; la lista anidada también se copia |
| dict (diccionario) | {"a": 1, "b": {"c": 2}} | Un nuevo diccionario, pero el {"c": 2} anidado es compartido | Un diccionario independiente y todos los diccionarios anidados |
| set (conjunto) | {1, 2, (3, 4)} | Un nuevo conjunto; los elementos mutables anidados serían compartidos | Un nuevo conjunto; todos los elementos anidados se copian recursivamente |
| tuple (tupla) | (1, 2, [3, 4]) | Devuelve la misma tupla, pero la lista anidada es compartida | Una nueva tupla con una copia de la lista anidada |
| str, int, float, bool | "hola", 42, 3.14, True | Devuelve el mismo objeto (son inmutables, no es necesario copiarlos) | Lo mismo que la copia superficial |
| clase personalizada | Nodo(1, Nodo(2)) | Un nuevo objeto de nivel superior; los objetos anidados son compartidos | Una copia completamente independiente del objeto y de todos los objetos anidados |
objeto con __slots__ | Una clase con __slots__ | Solo se copia el objeto de nivel superior | Todos los objetos anidados se copian si admiten la copia |
| archivo / socket / stream | open("file.txt") | Error (objeto no copiable) | Error (objeto no copiable) |
🧪 Ejemplos prácticos para la tabla
📌 list
import copy
a = [1, 2, [3, 4]]
b = copy.copy(a)
c = copy.deepcopy(a)
a[2][0] = 99
print(f"copy: {b}") # [1, 2, [99, 4]]
print(f"deepcopy: {c}") # [1, 2, [3, 4]]
📌 dict
import copy
a = {"a": 1, "b": {"c": 2}}
b = copy.copy(a)
c = copy.deepcopy(a)
a["b"]["c"] = 42
print(f"copy: {b}") # {'a': 1, 'b': {'c': 42}}
print(f"deepcopy: {c}") # {'a': 1, 'b': {'c': 2}}
📌 set
import copy
a = {1, 2, (3, 4)} # Los conjuntos solo pueden contener tipos inmutables
b = copy.copy(a)
c = copy.deepcopy(a)
print(b == c) # True (pero son dos objetos independientes en memoria)
📌 tuple
import copy
a = (1, 2, [3, 4]) # La tupla es inmutable, pero su elemento de lista no lo es
b = copy.copy(a)
c = copy.deepcopy(a)
a[2][0] = 99
print(f"copy: {b}") # (1, 2, [99, 4])
print(f"deepcopy: {c}") # (1, 2, [3, 4])
📌 Clase personalizada
import copy
class Nodo:
def __init__(self, valor, hijo=None):
self.valor = valor
self.hijo = hijo
a = Nodo(1, Nodo(2))
b = copy.copy(a)
c = copy.deepcopy(a)
a.hijo.valor = 99
print(f"copy: {b.hijo.valor}") # 99
print(f"deepcopy: {c.hijo.valor}") # 2
📌 Personalización del comportamiento: __copy__ y __deepcopy__
Puedes controlar cómo se copian tus propios objetos definiendo métodos especiales en su clase.
import copy
class Personalizada:
def __init__(self, x):
self.x = x
def __copy__(self):
print("¡Se llamó a __copy__!")
# Devuelve una nueva instancia, pero con la misma referencia a los datos internos
return Personalizada(self.x)
def __deepcopy__(self, memo):
# memo es un diccionario para rastrear objetos ya copiados (previene la recursión)
print("¡Se llamó a __deepcopy__!")
# Crea una copia completamente nueva, incluyendo los datos anidados
return Personalizada(copy.deepcopy(self.x, memo))
obj = Personalizada([1, 2])
c1 = copy.copy(obj) # Salida: ¡Se llamó a __copy__!
c2 = copy.deepcopy(obj) # Salida: ¡Se llamó a __deepcopy__!
⚡ Rendimiento y posibles problemas
- Velocidad:
copy.copy()es significativamente más rápido porque no recorre recursivamente toda la estructura de datos.copy.deepcopy()puede ser muy lento en objetos grandes y profundamente anidados. - Objetos no copiables: Los recursos del sistema como archivos abiertos, sockets o flujos de datos no se pueden copiar. Intentar hacerlo generará un error.
- Referencias cíclicas:
deepcopyes lo suficientemente inteligente como para manejar referencias cíclicas (cuando un objetoAse refiere aB, yBse refiere de nuevo aA) sin caer en un bucle infinito.
Cuándo usar cada uno: Las reglas principales
- Usa una copia superficial (
copy.copy()) si tu objeto solo contiene datos inmutables o si intencionadamente quieres que la copia y el original compartan objetos anidados. Es rápido y eficiente. - Usa una copia profunda (
copy.deepcopy()) cuando necesites una copia completa y 100% independiente del original, especialmente al trabajar con estructuras de datos complejas (listas, diccionarios, instancias de clase). - Para objetos inmutables (números, cadenas, tuplas sin elementos mutables), la copia generalmente no tiene sentido; Python ya optimiza su uso.
- Recuerda que puedes personalizar el proceso con
__copy__y__deepcopy__para tener un control total.