Imagina que tienes una lista con datos de un usuario:
usuario_original = {
'nombre': 'Juan',
'edad': 25,
'direccion': {
'calle': 'Gran Vía',
'ciudad': 'Madrid'
}
} Ahora quieres hacer una copia para trabajar sin afectar el original:
usuario_copia = usuario_original.copy() # Cambias la copia usuario_copia['nombre'] = 'Carlos' print(usuario_original['nombre']) # Juan print(usuario_copia['nombre']) # Carlos # Bien, la copia no afectó al original
Pero ahora cambias la ciudad en la copia:
usuario_copia['direccion']['ciudad'] = 'Barcelona' print(usuario_original['direccion']['ciudad']) # Barcelona print(usuario_copia['direccion']['ciudad']) # Barcelona # ¡¿QUÉ?! El original también cambió!
Acabas de encontrar posiblemente el bug más silencioso de Python: el shallow copy. Tu código “parece” correcto. Hiciste una copia. Pero cambió el original de todas formas.
La razón: .copy() hace una copia superficial. Copia el contenedor, pero no lo que hay dentro. Si hay estructuras anidadas (listas dentro de listas, diccionarios dentro de diccionarios), seguirán apuntando a los MISMOS objetos. Cambias uno, cambias ambos.
En esta entrada vamos a ver:
Imagina una estructura anidada como una caja dentro de otra caja:
datos = {
'nivel1': [1, 2, {'nivel2': 'valor'}]
} datos (original)
├── 'nivel1' → lista A
├── 1
├── 2
└── dict B → {'nivel2': 'valor'}
datos_copia = datos.copy()
├── 'nivel1' → lista A (¡MISMA referencia!)
├── 1
├── 2
└── dict B → {'nivel2': 'valor'} (¡MISMA referencia!) Shallow copy copia las “cajas exteriores” pero los contenidos anidados siguen siendo los MISMOS objetos. Modificas el contenido anidado en la copia y afectas al original.
datos (original)
├── 'nivel1' → lista A
├── 1
├── 2
└── dict B → {'nivel2': 'valor'}
datos_copia = deepcopy(datos)
├── 'nivel1' → lista A' (NUEVA copia)
├── 1
├── 2
└── dict B' → {'nivel2': 'valor'} (NUEVA copia) Deep copy recorre toda la estructura y copia TODO, incluso los objetos anidados. Ahora sí son completamente independientes.
usuario_original = {
'nombre': 'Juan',
'datos': {
'edad': 25,
'ciudad': 'Madrid'
}
}
# Shallow copy
usuario_copia = usuario_original.copy()
# Modificas la copia
usuario_copia['datos']['ciudad'] = 'Barcelona'
# ¡El original cambió!
print(usuario_original['datos']['ciudad']) # Barcelona
print(usuario_copia['datos']['ciudad']) # Barcelona ¿Por qué? Porque copy() solo copia el diccionario exterior. La clave 'datos' sigue apuntando al MISMO diccionario anidado en ambos.
print(id(usuario_original['datos'])) # 140234567891648 print(id(usuario_copia['datos'])) # 140234567891648 (¡MISMA dirección!)
matriz_original = [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9]
]
# Shallow copy
matriz_copia = matriz_original.copy()
# Modificas la copia
matriz_copia[0][0] = 999
# ¡La original también cambió!
print(matriz_original[0][0]) # 999
print(matriz_copia[0][0]) # 999 Las listas interiores son las MISMAS en ambas.
tareas_original = [
{'id': 1, 'titulo': 'Tarea 1', 'completada': False},
{'id': 2, 'titulo': 'Tarea 2', 'completada': False},
]
# Shallow copy
tareas_copia = tareas_original.copy()
# Marcar como completada en la copia
tareas_copia[0]['completada'] = True
# ¡La original también se marcó!
print(tareas_original[0]['completada']) # True
print(tareas_copia[0]['completada']) # True Necesitas importar deepcopy:
from copy import deepcopy
usuario_original = {
'nombre': 'Juan',
'datos': {
'edad': 25,
'ciudad': 'Madrid'
}
}
# Deep copy: copia TODO recursivamente
usuario_copia = deepcopy(usuario_original)
# Ahora sí puedes modificar sin afectar el original
usuario_copia['datos']['ciudad'] = 'Barcelona'
print(usuario_original['datos']['ciudad']) # Madrid
print(usuario_copia['datos']['ciudad']) # Barcelona Ahora sí son completamente independientes:
print(id(usuario_original['datos'])) # 140234567891648 print(id(usuario_copia['datos'])) # 140234567991712 (¡DIFERENTE!)
Las tuplas tienen un comportamiento raro:
tupla_original = (1, 2, [3, 4, 5]) # "Copiar" una tupla tupla_copia = tupla_original # Modificar la lista dentro tupla_copia[2][0] = 999 print(tupla_original) # (1, 2, [999, 4, 5]) cambió!
¿Por qué? Porque las tuplas son inmutables pero contienen referencias. No puedes cambiar la tupla, pero sí lo que hay dentro.
# Intentar cambiar la tupla misma tupla_copia[0] = 10 # TypeError: 'tuple' object does not support item assignment # Pero cambiar el contenido... funciona tupla_copia[2][0] = 999 # Esto sí
# Diccionario
d = {'a': 1, 'b': [2, 3]}
d_copy = d.copy()
d_copy2 = dict(d) # También funciona
# Lista
l = [1, 2, [3, 4]]
l_copy = l.copy()
l_copy2 = l[:] # Slicing también copia
l_copy3 = list(l) # Constructor también
# Set
s = {1, 2, 3}
s_copy = s.copy()
# Tupla (no se copia, son immutables)
t = (1, 2, 3)
t_copy = t # No necesitas copiar from copy import deepcopy
# Cualquier estructura
d = {'a': 1, 'b': [2, {'c': 3}]}
d_deep = deepcopy(d)
l = [1, [2, [3, 4]]]
l_deep = deepcopy(l)
t = (1, [2, 3], {'a': 4})
t_deep = deepcopy(t) # Shallow copy es suficiente
datos = {
'nombre': 'Juan',
'edad': 25,
'activo': True
}
copia = datos.copy()
copia['nombre'] = 'Carlos' # Solo cambias valores simples
# No afecta al original # Deep copy es necesario
datos = {
'usuario': 'Juan',
'perfil': {
'edad': 25,
'hobbies': ['lectura', 'deportes']
}
}
copia = deepcopy(datos)
copia['perfil']['hobbies'].append('música') # No afecta al original Deep copy recorre toda la estructura recursivamente, por lo que es necesariamente más lento que una copia normal.
import timeit
from copy import deepcopy
datos = {'nivel1': {'nivel2': {'nivel3': {'nivel4': list(range(1000))}}}}
# Shallow copy
tiempo_shallow = timeit.timeit(
lambda: datos.copy(),
number=100000
)
# Deep copy
tiempo_deep = timeit.timeit(
lambda: deepcopy(datos),
number=100000
)
print(f"Shallow: {tiempo_shallow:.4f}s") # ~0.01s
print(f"Deep: {tiempo_deep:.4f}s") # ~0.5s (50 veces más lento!) Para estructuras profundas, deep copy puede ser notablemente más lento. Úsalo solo cuando lo necesites.
# Template de configuración
config_template = {
'tema': 'oscuro',
'lenguaje': 'es',
'notificaciones': {
'email': True,
'push': False,
'sms': False
}
}
# Crear configuración para cada usuario
def crear_config_usuario(user_id):
from copy import deepcopy
# Necesita deep copy porque modificaremos notificaciones
config = deepcopy(config_template)
# Personalizar sin afectar el template
config['notificaciones']['email'] = False
return config
config1 = crear_config_usuario(1)
config2 = crear_config_usuario(2)
# Cada una es independiente
config1['notificaciones']['email'] = False
print(config2['notificaciones']['email']) # True (no cambió) Si usases copy() en lugar de deepcopy(), cambiar notificaciones en config1 afectaría a config2.
from copy import deepcopy
def procesar_datos(datos_originales):
# Necesitas deep copy para no afectar los originales
datos = deepcopy(datos_originales)
# Transformación 1
for item in datos:
item['procesado'] = True
item['timestamp'] = "2024-01-15"
# Transformación 2
datos_filtrados = [d for d in datos if d.get('activo')]
return datos_filtrados, datos
originales = [
{'id': 1, 'activo': True, 'datos': {'valor': 100}},
{'id': 2, 'activo': False, 'datos': {'valor': 200}},
]
procesados, transformados = procesar_datos(originales)
print(originales[0]) # Sin 'procesado' ni 'timestamp'
print(procesados[0]) # Con 'procesado' y 'timestamp' Con shallow copy, los cambios en item['datos']['valor'] afectarían a los originales.
from copy import deepcopy
class Proyecto:
def __init__(self, nombre, tareas):
self.nombre = nombre
self.tareas = tareas # lista de diccionarios
proyecto1 = Proyecto("App", [
{'titulo': 'Diseño', 'completada': False},
{'titulo': 'Backend', 'completada': False},
])
# Crear una copia sin afectar el original
proyecto2 = deepcopy(proyecto1)
proyecto2.nombre = "Web"
proyecto2.tareas[0]['completada'] = True
print(proyecto1.tareas[0]['completada']) # False
print(proyecto2.tareas[0]['completada']) # True # Datos simples sin nesting
coordenadas_originales = {
'x': 10,
'y': 20,
'nombre': 'punto_a'
}
# Shallow copy está bien
coordenadas_copia = coordenadas_originales.copy()
coordenadas_copia['x'] = 15
coordenadas_copia['nombre'] = 'punto_b'
# El original no cambió
print(coordenadas_originales['x']) # 10
print(coordenadas_originales['nombre']) # punto_a Síntoma: cambias algo en la “copia” y el original también cambia. Sin razón aparente.
Solución: busca .copy() en el código. Pregúntate: “¿hay estructuras anidadas?” Si sí, cámbialo a deepcopy().
# Probablemente un bug lista_copia = lista_original.copy() # Cambias algo anidado lista_copia[0]['campo'] = "nuevo valor" # Y el original cambió print(lista_original[0]['campo']) # "nuevo valor" # Arreglado from copy import deepcopy lista_copia = deepcopy(lista_original) # Ahora el original no cambia
Ten cuidado: muchas operaciones hacen shallow copy “silenciosamente”:
from copy import deepcopy
datos = {'a': [1, 2, 3]}
# Todas estas son shallow copies
copia1 = datos.copy()
copia2 = dict(datos)
copia3 = {**datos} # Unpacking
copia4 = datos[:] # Slicing (solo para listas)
# Todas comparten la lista interna
copia1['a'].append(999)
print(datos) # {'a': [1, 2, 3, 999]} ← cambió! Si hay nesting, todas fallan de la misma forma. Usa deepcopy().
= tampoco copiaUna trampa extra:
datos_original = {'a': [1, 2, 3]}
datos_copia = datos_original # Esto NO es una copia
datos_copia['a'].append(999)
print(datos_original) # {'a': [1, 2, 3, 999]} ← ¡cambió! = no copia nada. Solo crea un alias. Ambas variables apuntan al MISMO objeto.
| Método | Qué copia | Cuándo usar | Velocidad |
|---|---|---|---|
= | Nada (alias) | Nunca (intención clara) | N/A |
.copy() | Solo nivel superior (shallow) | Datos sin nesting | Rápido |
[:] (slicing) | Solo nivel superior (shallow) | Listas sin nesting | Rápido |
deepcopy() | TODO recursivamente | Estructuras anidadas | Lento |
Shallow copy y deep copy parecen lo mismo (“copiar una estructura”), pero son fundamentalmente distintos:
copy(), [:], unpacking): copia el contenedor, no lo que hay dentro. Las estructuras anidadas siguen siendo REFERENCIAS al original.deepcopy()): recorre toda la estructura y copia TODO, incluso lo anidado. Verdadera independencia.El bug ocurre silenciosamente: tu código “funciona” hasta que modificas algo anidado. Ahí descubres que ambas estructuras comparten ese contenido.
Regla simple:
deepcopy().copy() está bien.deepcopy() (es más lento, pero correcto siempre).Imagen de Jill Wellington en Pixabay
Has aplicado un t-test y obtienes p = 0,003. El resultado es estadísticamente significativo, hay…
Hay un bug que aparece una y otra vez en código Python, tan sutil que…
Durante décadas, cuando escribías una dirección web y pulsabas Intro, tu navegador hacía algo que…
La independencia financiera es el punto en el que tus inversiones generan suficientes rendimientos para…
Tienes dos grupos de datos y quieres saber si son estadísticamente distintos. O tienes una…
A primera vista, tuplas y listas son casi idénticas. Ambas guardan colecciones ordenadas de elementos.…
This website uses cookies.