1.2 ndarray三要素:shape、dtype与strides


文档摘要

1.2 ndarray三要素:shape、dtype与strides 本节摘要:ndarray 由一块数据缓冲区和三个关键描述字段构成:dtype 决定每个元素占几个字节、怎么解释;shape 决定逻辑形状;strides 决定沿每个维度走一步跳多少字节。三要素合起来,任何一个元素的字节地址都能算出来。这是全书后续所有"视图还是拷贝"判断的地基。 一个数组在内存里到底长什么样 承接上一节:数组是一块连续内存。但你可能会问——二维数组也是连续的吗?转置、切片之后的数组呢?答案是:数据块永远是一维的字节序列,是三要素把它"解释"成了 N 维。 先建一个最普通的 2×3 数组,把它的身份证件全部打出来: 这串输出里最重要的是前三行。dtype 是 int64,即每个元素 8 字节;

1.2 ndarray三要素:shape、dtype与strides

本节摘要: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

本节要点回顾

  • 三要素:dtype 定元素宽度与解释方式,shape 定逻辑形状,strides 定每个维度一步跳多少字节
  • 地址公式:元素地址等于数据起始地址加各下标乘对应步长的和,定位是纯算术,无需查找
  • 对象头很轻:shape 与 strides 只是几十字节的元数据,改它们不动数据块
  • 读片习惯:拿到数组先看 shape、dtype、strides、itemsize 四件套
  • 转置即换步长:a.T 的 strides 从 (24, 8) 变 (8, 24),内存纹丝未动

下一节进入第 2 章,我们把 strides 这个字段单独拿出来细讲:步长如何决定访问模式,以及"步长不再规整"时会付出什么代价。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U