NumPy: 形状操作
形状操作
1. 你将学到
❶ reshape 不复制数据,只改元数据
❷ -1 自动推断维度大小
❸ flatten vs ravel:副本 vs 视图
❹ transpose / .T 转置原理
❺ newaxis 增维与 squeeze 降维
2. 一个开发者的真实故事
(1) 痛点
Bob 把 12 个数据从 3×4 变成 4×3,以为是"重新排列内存"——每次 reshape 都在拷贝数据,内存和速度都扛不住。
(2) 解法
Alice 用 Mermaid 图展示:"reshape 只改了元数据,数据一字节都没动——这就是 NumPy 快的秘密。" 只要操作返回的是视图,就不会有任何数据复制。
(3) 收益
理解"视图 vs 副本"后,Bob 写的形状操作代码内存占用降了一个数量级,速度也快了数倍。只有 flatten 是真的复制,其余 reshape/ravel/transpose/swapaxes 全是视图。
3. 形状操作详解
(1) reshape 原理
reshape 改变数组的形状,但不复制数据。它只修改数组的元数据(shape 和 strides),原始数据在内存中不变。
import numpy as np
a = np.arange(12)
b = a.reshape(3, 4)
print(a.shape) # (12,)
print(b.shape) # (3, 4)
print(b.base is a) # True — b is a view of a
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。
# reshape does NOT copy data
b[0, 0] = 999
print(a[0]) # 999 — modifying b changes a!
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。
reshape 内存图解
graph LR
A["Original Array<br/>shape=(12,)"] -->|"reshape(3,4)"| B["View<br/>shape=(3,4)"]
A -->|"reshape(4,3)"| C["View<br/>shape=(4,3)"]
A -->|"reshape(2,6)"| D["View<br/>shape=(2,6)"]
style A fill:#4CAF50,color:#fff
style B fill:#2196F3,color:#fff
style C fill:#FF9800,color:#fff
style D fill:#9C27B0,color:#fff
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。
核心要点:A、B、C、D 共享同一块内存,只是"解读方式"不同。
(2) -1 自动推断
reshape 中可以用 -1 让 NumPy 自动计算该维度的大小。
a = np.arange(12)
# NumPy infers: 12 / 3 = 4
b = a.reshape(3, -1) # shape = (3, 4)
# NumPy infers: 12 / 4 = 3
c = a.reshape(-1, 4) # shape = (3, 4)
# NumPy infers: 12 / 2 / 2 = 3
d = a.reshape(2, -1, 2) # shape = (2, 3, 2)
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。
规则:总元素数 = 各维度乘积,-1 位置由 NumPy 自动推算。
(3) C 顺序 vs F 顺序
| 特性 | C 顺序(行优先) | F 顺序(列优先) |
|---|---|---|
| 关键字 | order='C' |
order='F' |
| 遍历方向 | 最后一个维度变化最快 | 第一个维度变化最快 |
| 内存布局 | 行连续 | 列连续 |
| 默认 | ✅ NumPy 默认 | ❌ |
| 适用场景 | 大多数情况 | 与 Fortran/MATLAB 交互 |
a = np.arange(6).reshape(2, 3, order='C')
# [[0 1 2]
# [3 4 5]] — row-major
b = np.arange(6).reshape(2, 3, order='F')
# [[0 2 4]
# [1 3 5]] — column-major
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。
(4) flatten vs ravel
| 特性 | flatten() |
ravel() |
|---|---|---|
| 返回值 | 副本(copy) | 视图(view,如果可能) |
| 修改结果 | 不影响原数组 | 影响原数组 |
| 内存占用 | 高(新分配) | 低(共享) |
| 性能 | 较慢 | 较快 |
| 安全性 | ✅ 安全 | ⚠️ 修改会传播 |
a = np.arange(12).reshape(3, 4)
f = a.flatten() # copy
r = a.ravel() # view (usually)
f[0] = 999
print(a[0, 0]) # 0 — flatten is a copy, a unchanged
r[0] = 888
print(a[0, 0]) # 888 — ravel is a view, a changed!
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。
(5) transpose / .T
转置交换轴的顺序。对于 2D 数组,就是行列互换。
a = np.arange(12).reshape(3, 4)
# shape = (3, 4)
b = a.T # shape = (4, 3)
c = a.transpose() # same as .T
# For 3D arrays, specify axis order
d = np.arange(24).reshape(2, 3, 4)
e = d.transpose(2, 0, 1) # axes reordered: (4, 2, 3)
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。
| 方式 | 用法 | 适用场景 |
|---|---|---|
.T |
a.T |
2D 简洁转置 |
transpose() |
a.transpose() |
2D 转置 |
transpose(axes) |
a.transpose(2, 0, 1) |
多维自定义轴序 |
swapaxes() |
a.swapaxes(0, 1) |
交换指定两个轴 |
(6) newaxis 增维
np.newaxis(等价于 None)用于在指定位置增加长度为 1 的维度。
a = np.array([1, 2, 3]) # shape = (3,)
# Add column dimension
b = a[:, np.newaxis] # shape = (3, 1)
# Add row dimension
c = a[np.newaxis, :] # shape = (1, 3)
# Both ways produce the same result
d = a[:, None] # same as a[:, np.newaxis]
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。
| 增维方式 | 写法 | 说明 |
|---|---|---|
np.newaxis |
a[:, np.newaxis] |
可读性好 |
None |
a[:, None] |
简短等价写法 |
reshape |
a.reshape(-1, 1) |
显式指定形状 |
expand_dims |
np.expand_dims(a, axis=1) |
函数式写法 |
(7) squeeze 降维
squeeze 移除长度为 1 的维度。
a = np.arange(12).reshape(1, 3, 4, 1)
# shape = (1, 3, 4, 1)
b = np.squeeze(a) # shape = (3, 4)
c = np.squeeze(a, axis=0) # shape = (3, 4, 1)
d = np.squeeze(a, axis=3) # shape = (1, 3, 4)
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。
(8) swapaxes 交换轴
swapaxes 交换两个指定轴,是 transpose 的简化版本。
a = np.arange(24).reshape(2, 3, 4)
# shape = (2, 3, 4)
b = a.swapaxes(0, 1) # shape = (3, 2, 4)
c = a.swapaxes(1, 2) # shape = (2, 4, 3)
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。
▶ 示例
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。
:reshape 多种变形(难度⭐)
import numpy as np
a = np.arange(12)
print(f"Original: shape={a.shape}")
# Various valid reshapes
print(f"reshape(3,4): {a.reshape(3, 4).shape}")
print(f"reshape(4,3): {a.reshape(4, 3).shape}")
print(f"reshape(2,6): {a.reshape(2, 6).shape}")
print(f"reshape(6,2): {a.reshape(6, 2).shape}")
print(f"reshape(2,2,3): {a.reshape(2, 2, 3).shape}")
print(f"reshape(3,2,2): {a.reshape(3, 2, 2).shape}")
# All share the same memory
b = a.reshape(3, 4)
b[0, 0] = -1
print(f"a[0] after modifying b: {a[0]}") # -1
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。
▶ 示例
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。
:-1 自动推断(难度⭐)
import numpy as np
a = np.arange(24)
# Let NumPy infer one dimension
print(a.reshape(4, -1).shape) # (4, 6)
print(a.reshape(-1, 8).shape) # (3, 8)
print(a.reshape(2, -1, 4).shape) # (2, 3, 4)
print(a.reshape(-1, 2, 2, 2).shape) # (3, 2, 2, 2)
# Invalid: more than one -1
try:
a.reshape(-1, -1, 4) # ERROR: can only specify one unknown dimension
except ValueError as e:
print(f"Error: {e}")
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。
▶ 示例
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。
:flatten vs ravel 与 base 追踪(难度⭐⭐)
import numpy as np
a = np.arange(12).reshape(3, 4)
f = a.flatten()
r = a.ravel()
# Check who owns the data
print(f"flatten base is a? {f.base is a}") # False — copy
print(f"ravel base is a? {r.base is a}") # True — view
# Modify the flattened copy
f[0] = 9999
print(f"After f[0]=9999, a[0,0]={a[0, 0]}") # 0 — unchanged
# Modify the raveled view
r[0] = 8888
print(f"After r[0]=8888, a[0,0]={a[0, 0]}") # 8888 — changed!
# For non-contiguous arrays, ravel may also copy
a_f = np.arange(12).reshape(3, 4, order='F')
r_f = a_f.ravel(order='C')
print(f"F-order ravel C-order base: {r_f.base is a_f}") # may be False
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。
▶ 示例
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。
:transpose / .T 多维转置(难度⭐⭐)
import numpy as np
# 2D transpose
a = np.arange(12).reshape(3, 4)
print(f"Original shape: {a.shape}") # (3, 4)
print(f"a.T shape: {a.T.shape}") # (4, 3)
print(f"a[1,2] = {a[1, 2]}") # 6
print(f"a.T[2,1] = {a.T[2, 1]}") # 6 — same data
# 3D transpose with custom axis order
b = np.arange(24).reshape(2, 3, 4)
print(f"b.shape = {b.shape}") # (2, 3, 4)
c = b.transpose(2, 0, 1)
print(f"b.transpose(2,0,1) = {c.shape}") # (4, 2, 3)
# Verify element correspondence
print(f"b[1, 2, 3] = {b[1, 2, 3]}") # 23
print(f"c[3, 1, 2] = {c[3, 1, 2]}") # 23
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。
▶ 示例
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。
:newaxis 增维与广播(难度⭐⭐)
import numpy as np
a = np.array([1, 2, 3]) # shape (3,)
b = np.array([10, 20, 30, 40]) # shape (4,)
# Outer product via newaxis
outer = a[:, np.newaxis] * b[np.newaxis, :]
print(f"Outer product shape: {outer.shape}") # (3, 4)
print(outer)
# [[ 10 20 30 40]
# [ 20 40 60 80]
# [ 30 60 90 120]]
# Equivalent with reshape
outer2 = a.reshape(-1, 1) * b.reshape(1, -1)
print(f"Same result? {np.array_equal(outer, outer2)}") # True
# expand_dims is the function version
c = np.expand_dims(a, axis=0) # shape (1, 3)
d = np.expand_dims(a, axis=1) # shape (3, 1)
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。
▶ 示例
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。
:squeeze 与 swapaxes(难度⭐⭐)
import numpy as np
# squeeze: remove size-1 dimensions
a = np.arange(12).reshape(1, 3, 4, 1)
print(f"Before squeeze: {a.shape}") # (1, 3, 4, 1)
print(f"After squeeze all: {np.squeeze(a).shape}") # (3, 4)
print(f"Squeeze axis=0: {np.squeeze(a, 0).shape}") # (3, 4, 1)
print(f"Squeeze axis=3: {np.squeeze(a, 3).shape}") # (1, 3, 4)
# swapaxes: swap two specific axes
b = np.arange(24).reshape(2, 3, 4)
print(f"Original: {b.shape}") # (2, 3, 4)
print(f"swapaxes(0,1): {b.swapaxes(0, 1).shape}") # (3, 2, 4)
print(f"swapaxes(1,2): {b.swapaxes(1, 2).shape}") # (2, 4, 3)
# swapaxes is its own inverse
c = b.swapaxes(0, 1).swapaxes(0, 1)
print(f"Double swap back: {np.shares_memory(b, c)}") # True
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。
4. 综合示例
▶ 示例
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。
:8×8 灰度图像全流程(难度⭐⭐⭐)
场景:Charlie 有一张 8×8 灰度图像数据,依次做 reshape→flatten→ravel→transpose,全程验证内存共享。
import numpy as np
# Charlie's 8x8 grayscale image (64 pixels)
img = np.arange(64, dtype=np.float32).reshape(8, 8)
print(f"1. Original image shape: {img.shape}")
print(f" img[0,0]={img[0,0]}, img[7,7]={img[7,7]}")
# Step 1: reshape to 4x16 (4 rows, 16 cols)
img_4x16 = img.reshape(4, 16)
print(f"\n2. After reshape(4,16): {img_4x16.shape}")
print(f" Shares memory with original? {np.shares_memory(img, img_4x16)}")
# Step 2: reshape to 4x4x4 (4 blocks, 4 rows, 4 cols)
img_4x4x4 = img.reshape(4, 4, 4)
print(f"\n3. After reshape(4,4,4): {img_4x4x4.shape}")
print(f" Shares memory with original? {np.shares_memory(img, img_4x4x4)}")
# Step 3: ravel (view)
img_ravel = img.ravel()
print(f"\n4. After ravel: {img_ravel.shape}")
print(f" Shares memory with original? {np.shares_memory(img, img_ravel)}")
# Step 4: flatten (copy)
img_flatten = img.flatten()
print(f"\n5. After flatten: {img_flatten.shape}")
print(f" Shares memory with original? {np.shares_memory(img, img_flatten)}")
# Step 5: transpose the reshaped view
img_4x16_T = img_4x16.T
print(f"\n6. After transpose of (4,16): {img_4x16_T.shape}")
print(f" Shares memory with img_4x16? {np.shares_memory(img_4x16, img_4x16_T)}")
# Step 6: Modify through view chain
img_ravel[0] = 999.0
print(f"\n7. After ravel[0]=999.0:")
print(f" img[0,0]={img[0,0]}") # 999.0 — view chain propagated
print(f" img_4x16[0,0]={img_4x16[0,0]}") # 999.0
img_flatten[1] = 777.0
print(f" img[0,1]={img[0,1]}") # 1.0 — flatten is a copy, unchanged
# Step 7: Restore and swapaxes
img[0, 0] = 0.0
img_swapped = img.reshape(8, 8, 1).swapaxes(0, 2)
print(f"\n8. After swapaxes(0,2) on (8,8,1): {img_swapped.shape}")
print(f" Shares memory? {np.shares_memory(img, img_swapped)}")
# Summary table
print("\n=== Memory Sharing Summary ===")
ops = {
'reshape(4,16)': img_4x16,
'reshape(4,4,4)': img_4x4x4,
'ravel()': img_ravel,
'flatten()': img_flatten,
'transpose(.T)': img_4x16_T,
'swapaxes(0,2)': img_swapped,
}
for name, arr in ops.items():
shared = np.shares_memory(img, arr)
print(f" {name:20s} | shares_memory={shared} | view={'Yes' if shared else 'No (copy)'}")
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。
❓ 常见问题
np.arange(12).reshape(5, 3) 会报 ValueError,因为 5×3=15 ≠ 12。ravel()——省内存、速度快。只有当你需要确保修改不会影响原数组时,才用 flatten()。reshape 中最多只能出现一个 -1,否则 NumPy 无法唯一确定每个维度的大小,会抛出 ValueError。a.transpose(i, j, k) 把原来的轴 0→i 位置、轴 1→j 位置、轴 2→k 位置。等价地,result[x_i, x_j, x_k] = a[x_0, x_1, x_2]。可以理解为"重排轴的顺序"。newaxis 更直观(在切片中插入维度),reshape 更显式(直接指定目标形状)。选择看个人偏好。np.shares_memory() 检查。📖 小节
reshape— 不复制(视图),关键用法:a.reshape(3, 4)/a.reshape(-1, 3)ravel— 不复制(通常视图),关键用法:a.ravel()flatten— 复制(副本),关键用法:a.flatten().T/transpose— 不复制(视图),关键用法:a.T/a.transpose(2, 0, 1)newaxis— 不复制(视图),关键用法:a[:, np.newaxis]squeeze— 不复制(视图),关键用法:np.squeeze(a)/np.squeeze(a, axis=0)swapaxes— 不复制(视图),关键用法:a.swapaxes(0, 1)
核心心法:reshape/ravel/transpose/swapaxes 都是"换眼镜不换桌子"——数据不动,只是解读方式变了。只有 flatten 是真的"复印一份"。
📝 作业
(1) 6 种 reshape
对 np.arange(60),写出 6 种不同的合法 reshape 形状(如 (6,10)),并打印每种结果的 shape。
(2) 验证 ravel 视图与 flatten 副本
- 创建
a = np.arange(20).reshape(4, 5) - 分别调用
r = a.ravel()和f = a.flatten() - 修改
r[0] = 999,检查a[0, 0]是否变化 - 修改
f[1] = 777,检查a[0, 1]是否变化 - 用
np.shares_memory()验证
(3) newaxis 1D→3D
- 创建
a = np.array([10, 20, 30, 40, 50])(shape(5,)) - 用
newaxis将其变为 shape(1, 5, 1) - 再用
squeeze还原为(5,) - 验证每步的 shape 和内存共享情况