Vistas de arreglos y copias superficiales: precaución con la propagación de cambios
Al finalizar este tema
Comprenderás la diferencia entre vistas (view) y copias (copy) en NumPy, podrás explicar por qué el slicing devuelve una vista y evitarás la propagación no intencionada de cambios.
Comportamiento sorprendente
import numpy as np
original = np.array([1, 2, 3, 4, 5])sliced = original[1:4]
sliced[0] = 99
print(sliced) # [99 3 4]print(original) # [ 1 99 3 4 5] ← ¡También cambia el original!sliced solo se modificó, pero original también cambió. Esto no es un error, sino una característica de diseño. El slicing de NumPy no copia los datos, sino que devuelve una vista (view) que comparte la misma memoria.
¿Qué es una vista (view)?
Una vista es ver los mismos datos desde una perspectiva diferente.
Memoria: [1] [2] [3] [4] [5]
↑ ↑ ↑ ↑ ↑
original: [0] [1] [2] [3] [4]
sliced = original[1:4]
↑ ↑ ↑
sliced: [0] [1] [2] ← ¡Apunta a la misma memoria!Al no copiarse los datos:
- Ahorro de memoria: Una vista de un array de 1 GB no requiere memoria adicional.
- Rapidez: El tiempo de copia es cero.
- Propagación de cambios: Si se modifica la vista, también se modifica el original (¡atención!).
Cuándo se crea una vista
arr = np.array([1, 2, 3, 4, 5, 6, 7, 8, 9])
# 1. Slicing → vistav1 = arr[2:5] # Vistav1.base is arr # True (es una vista de arr)
# 2. reshape → vista cuando es posiblev2 = arr.reshape(3, 3) # Vistav2.base is arr # True
# 3. Transposición → vistamat = np.array([[1, 2], [3, 4]])v3 = mat.T # Vistav3.base is mat # True
# 4. Conversión de dtype con el mismo tamaño → vistav4 = arr.view(np.int64) # VistaCómo verificar las vistas
arr = np.array([1, 2, 3, 4, 5])sliced = arr[1:4]copied = arr[1:4].copy()
print(sliced.base is arr) # True: vistaprint(copied.base is None) # True: copia independiente sin baseSi base es None, el propio objeto es el dueño de los datos (una copia); si no es None, es una vista de otro array.
Cuándo se produce una copia
arr = np.array([1, 2, 3, 4, 5])
# 1. Indexación avanzada → copiac1 = arr[[0, 2, 4]] # ¡Copia!c1[0] = 99print(arr) # [1 2 3 4 5] — original sin cambios
# 2. Indexación booleana → copiac2 = arr[arr > 3] # ¡Copia!c2[0] = 99print(arr) # [1 2 3 4 5] — original sin cambios
# 3. Copia explícitac3 = arr.copy() # Copiac3[0] = 99print(arr) # [1 2 3 4 5] — original sin cambios| Operación | Resultado |
|---|---|
arr[2:5] (slicing) | Vista |
arr[[0,2,4]] (fancy indexing) | Copia |
arr[arr > 3] (indexación booleana) | Copia |
arr.reshape(...) | Vista (si es posible) |
arr.copy() | Copia |
arr.flatten() | Copia |
arr.ravel() | Vista (si es posible) |
Diferencias con pandas
import pandas as pd
df = pd.DataFrame({"A": [1, 2, 3], "B": [4, 5, 6]})
# Slicing en pandassubset = df[df["A"] > 1]subset["B"] = 99# Puede producirse SettingWithCopyWarningEl comportamiento de vista/copia en pandas varía según el contexto, lo que dificulta su predicción. Por ello existe el SettingWithCopyWarning. La forma segura es:
# Copia inequívocasubset = df[df["A"] > 1].copy()subset["B"] = 99 # Original sin cambios y sin advertencia
# Si se desea modificar el originaldf.loc[df["A"] > 1, "B"] = 99 # Modificación directaErrores comunes en la práctica
Modificación del objeto original dentro de una función
def normalize(data): # Mal: si data es una vista, modifica el original data -= data.mean() return data
original = np.array([10.0, 20.0, 30.0])result = normalize(original[0:3]) # ¡Slicing = vista!print(original) # [−10. 0. 10.] ← ¡El original ha cambiado!
# Bien: trabaja sobre una copiadef normalize_safe(data): result = data.copy() result -= result.mean() return resultUso de vistas en grandes volúmenes de datos
# Analiza solo una parte de 10 GB de datoshuge_data = np.memmap("data.bin", dtype=np.float64, shape=(1_000_000_000,))
# Vista: 0 memoria adicionalchunk = huge_data[1000:2000] # Vista: no copia memoria
# Copia: asigna memoriachunk_copy = huge_data[1000:2000].copy() # Copia: asigna 8 KBEn conjuntos de datos de gran tamaño, se utilizan intencionalmente las vistas para ahorrar memoria. Se llama a copy() solo cuando es necesario realizar modificaciones.
Comparación con listas de Python
El slicing de las listas de Python, a diferencia de NumPy, siempre devuelve una copia.
# Lista de Python: slicing = siempre copia superficialpy_list = [1, 2, 3, 4, 5]sliced = py_list[1:4]sliced[0] = 99print(py_list) # [1, 2, 3, 4, 5] — ¡original sin cambios!
# NumPy: slicing = vista compartidanp_arr = np.array([1, 2, 3, 4, 5])sliced = np_arr[1:4]sliced[0] = 99print(np_arr) # [ 1 99 3 4 5] — ¡original modificado!Si no se comprende esta diferencia, pueden producirse errores graves al migrar a NumPy. El código que era seguro con las listas de Python puede modificar el objeto original en NumPy.
Copia superficial vs. copia profunda
import copy
nested = [[1, 2], [3, 4]]
# Copia superficial: copia la lista exterior y comparte las interioresshallow = copy.copy(nested)shallow[0][0] = 99print(nested) # [[99, 2], [3, 4]] — ¡la lista interior cambia!
# Copia profunda: copia también todos los objetos anidadosnested = [[1, 2], [3, 4]]deep = copy.deepcopy(nested)deep[0][0] = 99print(nested) # [[1, 2], [3, 4]] — original sin cambiosLa función .copy() de NumPy copia la totalidad de los datos, por lo que tiene el mismo efecto que una copia profunda. Dado que los arrays de NumPy no contienen otros objetos de Python en su interior (son datos numéricos puros), la distinción entre copia superficial y profunda carece de sentido, y .copy() es suficiente.
Diagrama de flujo de decisión
¿Necesitas el resultado de una operación con arrays?
├── ¿Se puede modificar el original?
│ ├── Sí → usa una vista (slicing directo)
│ └── No → llama a .copy()
└── ¿Hay memoria suficiente?
├── Sí → usa .copy() por seguridad
└── No → usa una vista y evita modificarlaResumen clave
| Concepto | Resumen |
|---|---|
| Vista (View) | Comparte la misma memoria. Las modificaciones se propagan al original |
| Copia (Copy) | Memoria independiente. Las modificaciones no afectan al original |
| Slicing | Devuelve una vista (NumPy) |
| Indexación avanzada/booleana | Devuelve una copia |
.base | Si es None, devuelve una copia; de lo contrario, una vista |
.copy() | Copia profunda explícita |
Las vistas en NumPy están diseñadas para la optimización del rendimiento. Permiten manejar datos de varios GB sin necesidad de copiarlos, pero conllevan el efecto secundario de que "si se modifica, también cambia el original". La regla es simple: slicing = vista, indexación avanzada/booleana = copia. Si no estás seguro, utiliza .copy().
Desde pandas 2.0, se ha introducido el modo Copy-on-Write (CoW), que realiza una copia automática al modificar los resultados de un slicing. Se puede activar con pd.options.mode.copy_on_write = True y está previsto que sea el comportamiento predeterminado en el futuro. Sin embargo, NumPy sigue utilizando un modelo explícito de vistas/copias, por lo que es fundamental dominar las reglas de este tema.