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 三种变换后的形状与内存行为差异,帮助建立"同一份数据、不同视图"的直觉。
工程实战:维度不匹配的典型场景与处理
维度不匹配是 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。.copy() 或 flatten() 显式复制,避免"数据被悄悄改掉"的隐蔽 bug。最后强调一个习惯:任何形状变换之后,立即用 assert 或打印校验结果形状。例如 assert frames.shape[1] == win。工程数据往往来自文件、接口或设备,形状带有不确定性,把校验写进代码,比事后调试更省时间。属性是"读",变换是"写结构",二者配合,才能让数组始终处在可计算、可验证的状态。