4. 数组形状变换

任务说明

本案例将使用NumPy的reshape、ravel、flatten方法对数组进行形状变换,并通过内存共享检测来区分视图(view)与副本(copy)的本质差异。完成本案例后,你将能够根据实际需求选择正确的形状变换方法,避免因视图共享导致的意外数据修改。

核心实现

import numpy as np

# 1. 创建原始数组(3行4列)
arr = np.arange(12).reshape(3, 4)
print("原始数组 arr (3x4):")
print(arr)
print("arr 的 shape:", arr.shape, "| 内存地址:", arr.__array_interface__['data'][0])
print()

# 2. reshape —— 返回新形状的数组(可能是视图,也可能是副本)
reshaped = arr.reshape(4, 3)
print("reshape(4,3) 结果:")
print(reshaped)
print("reshaped 的 shape:", reshaped.shape, "| 内存地址:", reshaped.__array_interface__['data'][0])
print("reshaped 与 arr 共享内存?", np.shares_memory(arr, reshaped))
print()

# 3. ravel —— 展平为一维,优先返回视图
raveled = arr.ravel()
print("ravel() 结果:")
print(raveled)
print("raveled 的 shape:", raveled.shape, "| 内存地址:", raveled.__array_interface__['data'][0])
print("raveled 与 arr 共享内存?", np.shares_memory(arr, raveled))
print()

# 4. flatten —— 展平为一维,总是返回副本
flattened = arr.flatten()
print("flatten() 结果:")
print(flattened)
print("flattened 的 shape:", flattened.shape, "| 内存地址:", flattened.__array_interface__['data'][0])
print("flattened 与 arr 共享内存?", np.shares_memory(arr, flattened))
print()

# 5. 验证视图与副本的行为差异
print("=== 修改 raveled[0] = 99(视图会改变原数组)===")
raveled[0] = 99
print("修改后 arr[0][0] =", arr[0][0], "(原数组被改变)")
print()

print("=== 修改 flattened[1] = 88(副本不会改变原数组)===")
flattened[1] = 88
print("修改后 arr[0][1] =", arr[0][1], "(原数组未被改变)")
print()

# 6. 非连续数组的 ravel 会强制返回副本
arr_slice = arr[:, ::2]  # 列步长为2,产生非连续视图
print("非连续切片 arr[:, ::2]:")
print(arr_slice)
print("arr_slice 的 flags:", arr_slice.flags['C_CONTIGUOUS'])
raveled_slice = arr_slice.ravel()
print("对非连续切片调用 ravel() 后,与原切片共享内存?", np.shares_memory(arr_slice, raveled_slice))

运行方式与依赖

本案例仅依赖NumPy库。运行前请确保已安装:

pip install numpy

将上述代码保存为shape_transform.py,在终端执行:

python shape_transform.py

验证方法与判定标准

运行程序后,按以下标准逐项核对输出:

  1. 内存地址对比:观察reshape、ravel、flatten输出的内存地址与原始数组的异同。若地址相同,说明共享内存(视图);若不同,则为副本。
  2. 共享内存标志:np.shares_memory返回True表示视图,False表示副本。预期结果:reshape(4,3)和ravel()对连续数组返回True,flatten()返回False。
  3. 修改传播验证:修改raveled[0]后,原数组arr[0][0]应变为99;修改flattened[1]后,原数组arr[0][1]应保持原值不变。
  4. 非连续数组行为:对列步长为2的切片调用ravel()时,由于切片在内存中不连续,NumPy会强制返回副本,np.shares_memory应返回False。

若以上4项输出与预期一致,则案例验证通过。

数组形状变换:视图与副本内存关系图 原始数组 arr shape: (3, 4) 内存块 A 连续内存 地址: 0x1000 reshape(4, 3) shape: (4, 3) 视图(通常) 共享内存块 A 地址: 0x1000 ravel() shape: (12,) 视图(连续时) 共享内存块 A 地址: 0x1000 flatten() shape: (12,) 副本(总是) 新内存块 B 地址: 0x2000 非连续切片 ravel() shape: (6,) 副本(强制) 新内存块 C 地址: 0x3000 共享 共享 复制数据 复制 视图(共享内存) 副本(独立内存) 共享引用 数据复制

关键结论

通过上述代码与图示可以明确:reshape在形状兼容且内存连续时返回视图,ravel优先返回视图但遇到非连续数组时自动转为副本,而flatten始终返回独立副本。判断视图与副本最可靠的方式是使用np.shares_memory()检查内存共享状态,而非仅凭方法名称猜测。当需要安全地修改展平结果而不影响原数组时,应使用flatten();当需要节省内存且明确不会意外修改原数据时,可使用ravel()或reshape()。