1.2 ndarray三要素:shape、dtype与strides 本节摘要:ndarray 由一块数据缓冲区和三个关键描述字段构成:dtype 决定每个元素占几个字节、怎么解释;shape 决定逻辑形状;strides 决定沿每个维度走一步跳多少字节。三要素合起来,任何一个元素的字节地址都能算出来。这是全书后续所有"视图还是拷贝"判断的地基。 一个数组在内存里到底长什么样 承接上一节:数组是一块连续内存。但你可能会问——二维数组也是连续的吗?转置、切片之后的数组呢?答案是:数据块永远是一维的字节序列,是三要素把它"解释"成了 N 维。 先建一个最普通的 2×3 数组,把它的身份证件全部打出来: 这串输出里最重要的是前三行。dtype 是 int64,即每个元素 8 字节;
本节摘要:ndarray 由一块数据缓冲区和三个关键描述字段构成:dtype 决定每个元素占几个字节、怎么解释;shape 决定逻辑形状;strides 决定沿每个维度走一步跳多少字节。三要素合起来,任何一个元素的字节地址都能算出来。这是全书后续所有"视图还是拷贝"判断的地基。
承接上一节:数组是一块连续内存。但你可能会问——二维数组也是连续的吗?转置、切片之后的数组呢?答案是:数据块永远是一维的字节序列,是三要素把它"解释"成了 N 维。
先建一个最普通的 2×3 数组,把它的身份证件全部打出来:
import numpy as np a = np.array([[1, 2, 3], [4, 5, 6]]) print("形状 shape:", a.shape) # (2, 3) print("类型 dtype:", a.dtype) # int64 print("步长 strides:", a.strides) # (24, 8) print("维度 ndim:", a.ndim) # 2 print("元素总数 size:", a.size) # 6 print("单元素字节 itemsize:", a.itemsize) # 8 print("总字节 nbytes:", a.nbytes) # 48
这串输出里最重要的是前三行。dtype 是 int64,即每个元素 8 字节;strides 是 (24, 8),意思是:行方向走一步跳 24 字节(3 个元素),列方向走一步跳 8 字节(1 个元素)。
有了 dtype 和 strides,元素 a[1, 2] 的地址就能直接算出来:
# 偏移字节数 = 行下标 * strides[0] + 列下标 * strides[1] offset = 1 * 24 + 2 * 8 # = 40 字节 print(a.ravel()[offset // 8]) # 6,正是 a[1, 2]
不需要任何魔法,NumPy 拿着"起始地址 + 偏移"就能直达那个字节。反过来说,只要不改数据块,仅靠修改 shape 和 strides 这两个轻量字段,就能"变"出转置、切片、 reshape 等等——这正是第 4、5 章大量零拷贝操作的原理。

这个套路值得养成习惯:拿到陌生数组,先打四样东西。用一个三维数组演练:
b = np.arange(24, dtype=np.float32).reshape(2, 3, 4) print("shape:", b.shape) # (2, 3, 4) print("dtype:", b.dtype) # float32 print("strides:", b.strides) # (48, 16, 4) print("itemsize:", b.itemsize) # 4 # 验算:strides 恰好等于后面所有维度大小的连乘积乘以 itemsize # 3*4*4=48, 4*4=16, 4=4 —— 对上了
float32 每元素 4 字节。strides (48, 16, 4) 告诉我们:第一维走一步跳过一整层(3×4 个元素),第二维走一步跳一行(4 个元素),第三维走一步就是相邻的 4 字节。当 strides 呈现这种"后缀连乘"形态时,数组是 C 序连续的——第 2 章会专门讲怎么检测与转换。
再看一个"不正常"的例子,预告后面章节的内容:
c = a.T # 转置 print("c 的 shape:", c.shape) # (3, 2) print("c 的 strides:", c.strides) # (8, 24) —— 和 a 相反! print("c 与 a 共享内存:", np.shares_memory(c, a)) # True
转置只是把 strides 从 (24, 8) 换成了 (8, 24),数据一个字节都没动。这就是"零拷贝"的第一个活例子,第 5 章会展开讲整套零拷贝家族。
⚠️ 常见坑:strides 为 0 的情况也存在。广播视图里会出现 strides 含 0 的数组,意味着沿该维度走多少步都停在同一个字节上。见到 strides 里的 0,要意识到这个维度是"虚"的。
检验三要素是否真的入脑,不看文档回答下面四问,答案都在本节代码里:
问:shape 为 (4,) 的一维 int32 数组,strides 是多少?
答:(4,)。一维数组沿唯一维度一步就是相邻元素,步长等于 itemsize。
问:nbytes、size、itemsize 三者关系?
答:nbytes 等于 size 乘 itemsize。size 是逻辑元素个数,itemsize 是单元素字节数,两者相乘才是数据块的真实体积。
问:两个数组 shape 相同、strides 不同,可能吗?
答:太可能了——a 和 a.T 的转置就是最典型的一对,逻辑形状可以相同但物理走法相反。这也是"光看 shape 不算认识数组"的证据。
问:修改 strides 会不会很慢?
答:恰恰相反,strides 是对象头里的几十字节元数据,改它是纳秒级操作。第 5 章的零拷贝变形家族全部建立在这个事实上。
import numpy as np # 一句话自测脚本:每问都能对上输出,三要素就算过关 v = np.arange(4, dtype=np.int32) print(v.shape, v.strides, v.size, v.itemsize, v.nbytes) # (4,) (4,) 4 4 16
下一节进入第 2 章,我们把 strides 这个字段单独拿出来细讲:步长如何决定访问模式,以及"步长不再规整"时会付出什么代价。