5.2 ravel、flatten与newaxis 本节摘要:ravel 尽量返回视图(可行时零拷贝),flatten 永远返回拷贝;newaxis 在指定位置插入长度为 1 的轴,squeeze 删除长度为 1 的轴。四者组合覆盖"展平与升降维"的全部日常需求,且直接决定第 6 章广播的成败。 展平双胞胎:一字之差,两份内存 选择标准一句话:后续只读就 ravel,要独立数据就 flatten 或 ravel 后 copy。flatten 的存在价值主要是语义明确——读代码的人一眼知道"这里要副本",不用回头查视图条件。 ravel 什么时候退化为拷贝?
本节摘要:ravel 尽量返回视图(可行时零拷贝),flatten 永远返回拷贝;newaxis 在指定位置插入长度为 1 的轴,squeeze 删除长度为 1 的轴。四者组合覆盖"展平与升降维"的全部日常需求,且直接决定第 6 章广播的成败。
import numpy as np m = np.arange(6).reshape(2, 3) r = m.ravel() # 尽量视图 f = m.flatten() # 永远拷贝 print(np.shares_memory(r, m)) # True print(np.shares_memory(f, m)) # False # r 是视图,写入穿透 r[0] = 99 print(m[0, 0]) # 99 # f 独立,改了白改 f[1] = -1 print(m[0, 1]) # 还是原值,没被影响
选择标准一句话:后续只读就 ravel,要独立数据就 flatten 或 ravel 后 copy。flatten 的存在价值主要是语义明确——读代码的人一眼知道"这里要副本",不用回头查视图条件。
ravel 什么时候退化为拷贝?和 reshape 一样,非连续数组:
import numpy as np m = np.arange(6).reshape(2, 3) print(np.shares_memory(m.T.ravel(), m)) # False,转置破坏了行连续性 # order 参数指定展平顺序:C 按行,F 按列 a = np.arange(6).reshape(2, 3) print(a.ravel(order="C")) # [0 1 2 3 4 5] print(a.ravel(order="F")) # [0 3 1 4 2 5]
newaxis(就是 None 的别名)在切片语法里插轴:
import numpy as np v = np.array([10, 20, 30]) # shape (3,) row = v[np.newaxis, :] # (1, 3) 行向量 col = v[:, np.newaxis] # (3, 1) 列向量 print(row.shape, col.shape) # (1, 3) (3, 1) # 等价写法 reshape 也行,但可读性差一截 print(v.reshape(1, -1).shape) # (1, 3) # squeeze 删除长度为1的轴 img = np.random.rand(1, 224, 224, 1) print(img.squeeze().shape) # (224, 224) print(img.squeeze(axis=0).shape) # (224, 224, 1) 指定轴更安全 # atleast_2d 家族:保底维度数 row2 = np.atleast_2d(v) # 变 (1, 3) col2 = np.atleast_2d(v).T # 变 (3, 1) print(row2.shape, col2.shape) # (1, 3) (3, 1)

完整过程。背景:给 5 个查询点和 8 个参考点算两两欧氏距离,朴素写法是双层循环 40 次;广播加 newaxis 可以零循环完成。
操作:
import numpy as np rng = np.random.default_rng(1) q = rng.random((5, 3)) # 5 个三维查询点 r = rng.random((8, 3)) # 8 个三维参考点 # 朴素循环版(对照用) import math loop = np.zeros((5, 8)) for i in range(5): for j in range(8): loop[i, j] = math.dist(q[i], r[j]) # newaxis 广播版:q 变 (5,1,3),r 变 (1,8,3),相减广播成 (5,8,3) diff = q[:, np.newaxis, :] - r[np.newaxis, :, :] dist = np.sqrt((diff ** 2).sum(axis=2)) print(dist.shape) # (5, 8) print(np.allclose(dist, loop)) # True,结果一致 print(dist[0].round(3)) # [0.538 0.631 0.404 0.634 0.519 0.432 0.388 0.561]
结果与解读:一次广播替代 40 次循环,数据量增大时差距拉大到百倍。关键一步是 q[:, np.newaxis, :]——在中间插一个长度 1 的轴,让 (5,1,3) 与 (1,8,3) 按第 6 章的规则对齐成 (5,8,3)。变式:若内存紧张(点数很大时 diff 中间数组是 N×M×3),改用展开公式 dist2 = (qq).sum(1)[:,None] + (rr).sum(1)[None,:] - 2*q@r.T,把中间数组压回 N×M——两种写法的选择本身就是"内存预算"思维。
⚠️ 常见坑:v[:, None] 与 v[None, :] 插轴位置写反是广播错误的第一大来源。写完先打 .shape 确认,再接运算,这一秒钟能省半小时调试。
ravel 与 flatten 都接受 order 参数,结果差异值得亲手看一次,这是理解 C 序 F 序最直观的练习题:
import numpy as np m = np.arange(6).reshape(2, 3) print(m.ravel()) # [0 1 2 3 4 5] 按行读 print(m.ravel(order="F")) # [0 3 1 4 2 5] 按列读 # 验证:按列读等价于先转置再按行读 print(np.array_equal(m.ravel(order="F"), m.T.ravel())) # True
同一块内存,两种读法得到不同序列——这句话就是"数据块与解释方式分离"的最后一道练习题。做图像或矩阵与外部系统(比如按列存储的数据库、Fortran 写的老求解器)交换数据时,order 参数就是那把对齐钥匙。
顺带一提展平的逆操作:一维升回多维用 reshape,配 -1 拼接也是常见组合,比如把一批展平后的向量 reshape(-1, 28, 28) 还原成图片堆——深度学习数据管线的标准动作。
形状工具备齐,第 6 章正式进入运算世界:元素级运算的内存账本,以及把这些形状技巧串成完整对齐规则的广播机制。