3. 数组属性与形状操作

在工程数据处理中,我们拿到一个数组后,第一件该做的事往往不是立刻计算,而是先"看清它长什么样"。NumPy 数组自带一组描述自身结构的属性,它们不改变数据,却能告诉我们这个数组有几个维度、每个维度多长、一共多少元素、元素是什么类型。掌握这些属性,再配合形状变换方法,就能解决工程中最常见的一类报错——维度不匹配。

四个核心属性:先认识数组的"身份证"

以一个采集到的振动信号为例:假设我们用三轴加速度计对一台电机采集了 1000 个采样点,每个采样点有 X、Y、Z 三个方向,构造出的数组形状为 (1000, 3)。围绕这个数组,四个属性分别回答不同问题:

  • shape:返回一个元组,表示每个维度的大小,这里是 (1000, 3),即 1000 行、3 列。它是理解数组结构最关键的属性。
  • ndim:维度个数,也叫轴数。上面数组 ndim 为 2,表示它是二维数组;一维信号序列 ndim 为 1。
  • size:元素总数,等于 shape 各分量之积,这里为 1000×3 = 3000。
  • dtype:元素数据类型,如 float64、int32、bool。它决定每个元素占多少字节,也影响精度与内存。
import numpy as np

sig = np.arange(3000, dtype=np.float64).reshape(1000, 3)
print(sig.shape)   # (1000, 3)
print(sig.ndim)    # 2
print(sig.size)    # 3000
print(sig.dtype)   # float64
print(sig.nbytes)  # 24000,即 3000 × 8 字节

这里顺带引入 nbytes:它等于 size 乘以 dtype 的字节宽度。工程中处理大数组时,先看 nbytes 能快速判断内存占用,避免一次性载入导致内存溢出。注意 shape 是元组,可以索引取值,例如 sig.shape[0] 得到 1000,sig.shape[1] 得到 3,这在写通用函数时非常常用。

形状变换:reshape、ravel、flatten、resize 的分工

形状变换的本质是"重新解释同一块数据的内存布局",元素顺序不变,只是维度和步长重新组织。理解这一点,就能预判变换后的结果。

reshape 是最常用的方法,它返回一个新视图(在内存连续时),不复制数据。调用时新形状各分量之积必须等于原 size,否则抛出 ValueError。可以用 -1 让 NumPy 自动推断某一维:

a = np.arange(12)
b = a.reshape(3, 4)      # 3 行 4 列
c = a.reshape(2, -1)     # 自动推断为 (2, 6)
d = a.reshape(3, 4).ravel()  # 拉平回一维,视图

ravel 与 flatten 都把数组拉平成一维,区别在于:ravel 尽量返回视图,若原数组内存不连续则复制;flatten 总是返回一份副本。因此对大数据,ravel 更省内存,但修改 ravel 结果可能影响原数组,需谨慎。

resize 有两种用法:ndarray.resize 原地修改数组本身,元素总数可变,多出的位置补 0,不足则截断;np.resize 则返回新数组并按需重复填充。工程中更常用 reshape,resize 多用于预分配缓冲区。

下表对比四种方法的关键差异,便于按场景选择:

方法是否改变原数组是否可改变元素总数返回视图或副本
reshape否否(size 不变)尽量视图
ravel否否尽量视图
flatten否否总是副本
resize(ndarray)是是原地

用一张图看清属性与形状变换的关系

下面的示意图把"属性读取"和"形状变换"放在同一条数据链上:左侧是原始二维数组及其四个属性,右侧展示 reshape、ravel、flatten 三种变换后的形状与内存行为差异,帮助建立"同一份数据、不同视图"的直觉。

数组属性与形状变换关系图 原始数组 sig 形状 (1000, 3) ndim = 2 size = 3000 dtype = float64 nbytes = 24000 变换 reshape(500, 6) size 不变,返回视图 元素顺序保持行优先 ravel() 拉平为一维 (3000,) 尽量视图,省内存 flatten() 拉平为一维 (3000,) 总是副本,安全但占内存 工程用途 • reshape:矩阵化 批量运算 • ravel:送入 一维滤波器 • flatten:需保留 原始数据时 • 变换前先核对 size 是否匹配 否则 ValueError 关键约束:reshape 前后 size 必须相等;-1 只能出现一次,用于自动推断

工程实战:维度不匹配的典型场景与处理

维度不匹配是 NumPy 使用中最常见的错误来源。举一个真实感较强的例子:我们有一组传感器数据,形状为 (1000, 3),代表 1000 个时刻的三轴读数;另有一个标定系数向量,形状为 (3,)。现在想对每个时刻的三个轴分别乘以对应系数。直接写 sig * coef 时,NumPy 会按广播规则把 (3,) 对齐到最后一维,结果正确。但如果系数被误存成 (3, 1),广播就会失败或产生意外形状。此时应先检查 coef.shape,再用 coef.reshape(3,) 或 coef.ravel() 修正。

另一个高频场景是"把一维长序列切成窗口"。假设采集到 12000 个点的单通道信号,想按每 200 点一个窗口、重叠 50% 切分。可以先计算窗口数,再用 reshape 配合切片实现:

raw = np.arange(12000, dtype=np.float64)
win, step = 200, 100
n = (raw.size - win) // step + 1
idx = np.arange(n)[:, None] * step + np.arange(win)
frames = raw[idx]          # 形状 (n, 200)
print(frames.shape)        # 例如 (119, 200)

这里没有直接 reshape,而是用索引构造二维窗口矩阵,因为重叠窗口无法用单纯 reshape 表达。这说明:reshape 适合"无重叠、元素总数不变"的重排;一旦涉及重叠或补零,就要借助索引、np.lib.stride_tricks 或 padding 手段。判断依据始终是——变换后 size 是否守恒、元素顺序是否符合预期。

排查维度问题的固定动作:打印 arr.shape 与 arr.size,确认目标形状各分量之积等于 size;若不等,先判断是需要截断、补零还是改变切分策略,再动手变换,而不是盲目加 -1。
reshape 返回的可能是视图,修改视图会影响原数组。若后续要保留原始数据用于对照或回滚,应使用 .copy() 或 flatten() 显式复制,避免"数据被悄悄改掉"的隐蔽 bug。

最后强调一个习惯:任何形状变换之后,立即用 assert 或打印校验结果形状。例如 assert frames.shape[1] == win。工程数据往往来自文件、接口或设备,形状带有不确定性,把校验写进代码,比事后调试更省时间。属性是"读",变换是"写结构",二者配合,才能让数组始终处在可计算、可验证的状态。