资讯详情

资讯详情

深入解析NumPy中数组的核心概念与操作技巧

前言NumPy 是第三方库不是标准库用之前要pip install numpy。本机没有 Python 解释器也没有装 NumPy所以下面的示例无法在本机运行验证只能逐行人工推演属性和行为的描述以 NumPy 官方文档为准。ndarray是 NumPy 的核心数据结构也是这个库几乎所有功能的基础。但很多人对它的理解停留在「一个能装数字的容器」于是遇到下面这些现象时完全不知道从哪查起改了一个切片原数组怎么也变了astype之后内存暴涨明明只是换了个类型两个形状不一样的数组相加居然没报错reshape有时候不复制数据有时候又复制。这些现象的答案全部在于ndarray不是一个「容器」而是两块东西的组合一块放数据的连续缓冲区一块描述「怎么解读这块缓冲区」的元数据。本文按「结构 → 属性 → 跨距 → 视图与副本 → 广播 → 变形」的顺序把这些概念串起来。顺带一句Python 2.7 已于 2020 年 1 月 1 日停止维护现在所有受支持的 NumPy 都只面向 Python 3网上老教程里的print语句、xrange等写法不要再照抄。一、ndarray的两部分与它的核心属性官方文档对ndarray的内部组织的说法很直白它由一段连续的一维数据缓冲区存放真正的元素和描述这块缓冲区的元数据组成。元数据里包括数据类型、形状、跨距等信息帮助 NumPy 知道「第 i 个数在缓冲区的哪个位置」。这个结构解释了两件在 NumPy 里反复出现的现象同一个缓冲区可以有多种「解读方式」。只要改元数据比如跨距就能得到一块「看起来不同」的数组而数据一个字节都没搬。这就是视图view。「数组的形状」和「数据在内存里怎么排」是两件事。形状是元数据内存布局由跨距描述二者可以不一致比如转置之后。理解了这一点后面所有「为什么改了这里那里也变了」的问题都有了统一的答案。核心属性shape/dtype/ndim/size这几个属性是最常用的先列清楚它们的准确含义避免混用属性含义类型例子a np.zeros((2, 3), dtypenp.float64)ndim维数轴的个数int2shape各轴的长度组成的元组tuple(2, 3)dtype元素的数据类型dtype对象float64size元素总个数int6itemsize单个元素占的字节数int8nbytes数组占用的总字节数int48注意size和nbytes的关系nbytes等于size乘以itemsize上面这个例子就是6 × 8 48。size是「多少个元素」nbytes是「多少字节」二者差一个itemsize的倍数——把它们混用在算内存时就会得出离谱的结论。# 适用于 Python 3.8 且已安装 NumPy以官方文档为准import numpy as npa np.zeros((2, 3), dtypenp.float64)print(a.ndim, a.shape, a.dtype)print(a.size, a.itemsize, a.nbytes)最后还要纠正一个常见误解shape不是「几行几列」。对二维数组它确实是「(行数, 列数)」但ndarray可以是任意维的那时shape是一串长度。用「行列」去理解遇到三维就会卡住。二、strides与内存布局strides是ndarray里最能体现「元数据」本质的属性它是一个元组每个元素表示「沿对应轴前进一格需要在内存缓冲区里跳多少个字节」。默认创建的ndarray是C 序也叫行优先row-major意思是最后一个轴变化最快——沿最后一轴走一步只跳一个itemsize沿第一根轴走一步要跳过整整一行。# 适用于 Python 3.8 且已安装 NumPy以官方文档为准import numpy as npa np.arange(6, dtypenp.int64).reshape(2, 3)print(a.strides) # (24, 8)跨一行 24 字节跨一列 8 字节print(a[0, 1]) # 1print(a[1, 0]) # 3因为按行优先存储int64的itemsize是 8所以沿最后一轴列跳一步是 8 字节一行有 3 个元素共 24 字节所以沿第一轴跳一步是 24 字节。跨距是理解转置、切片、ravel这些操作为什么快或为什么慢的关键。转置之所以几乎不花时间就是因为它只是把 strides 反转了没有搬数据# 适用于 Python 3.8 且已安装 NumPy以官方文档为准import numpy as npa np.arange(6, dtypenp.int64).reshape(2, 3)t a.Tprint(t.shape) # (3, 2)print(t.strides) # (8, 24)与 a 相反代价是转置后的数组不再连续。很多底层例程要求输入连续遇到非连续数组时会先做一次隐式复制这才是「转置之后操作变慢」的真正原因——不是转置本身慢而是后续操作不得不先规整内存。三、视图与副本这是全篇最实用的一节。官方文档的定义很清楚视图只改元数据、不改缓冲区所以改视图就是改原数组副本把缓冲区和元数据都复制一份改副本不影响原数组。判断方式是看base属性——视图的base指向原数组副本的base是None。哪些操作产视图、哪些产副本属于必须记牢的东西操作结果说明基本索引切片a[1:3]、a[:, 0]视图元素能用偏移和跨距表示花式索引a[[1, 2]]副本下标是数组无法用固定跨距表达布尔索引a[a 3]副本长度都不固定.T/transpose/swapaxes视图只改跨距reshape视图或副本能靠改跨距完成就是视图ravel尽量给视图保持连续时是视图flatten总是副本官方明确说明astype总是副本见下# 适用于 Python 3.8 且已安装 NumPy以官方文档为准import numpy as npa np.arange(9).reshape(3, 3)v a[0] # 切片视图v[0] 99print(a[0, 0]) # 99 —— 原数组被改c a[[1, 2]] # 花式索引副本c[0, 0] -1print(a[1, 0]) # 3 —— 原数组不变astype的语义要单独强调它按默认行为总是返回一份新数组即使目标dtype和当前完全相同也一样复制。它的签名里有一个copy参数默认是True把它设成False才允许在类型相同时直接返回原数组。所以在大数组上做循环转换时要小心——每一次astype都是一份新的内存占用。还有一条安全提醒不要用「原地改dtype」的方式去重新解读数据。虽然ndarray.dtype可以被赋值但那会改变对同一块缓冲区的解读方式稍有不慎就会把数据解释成完全不同的值。要转换类型用astype这类会明确产生新数组的接口。四、广播规则广播broadcasting是 NumPy 让形状不同的数组能做逐元素运算的机制。官方给出的规则可以浓缩成三句话从最后一个轴最右边开始逐个轴向左比较两个轴相等或者其中一个是 1就兼容不满足就抛ValueError操作数无法一起广播。参与运算的数组不必有相同的维数维数少的会被在左侧补上长度为 1 的轴。结果数组的维数等于输入中最大的那个每一轴的长度取各输入在该轴上的最大长度。a.shape: (3, 4)b.shape: ( 4) ← 左补一轴变成 (1, 4)结果: (3, 4)再举一个三维的例子用官方文档里的经典场景一批 256×256×3 的 RGB 数据想给三个颜色通道分别乘一个系数只需要乘一个长度为 3 的一维数组因为最右边的轴大小为 3对齐上了。广播不真的复制数据。官方强调所谓「把小的那个拉伸开」只是概念上的说法NumPy 会用原来的标量或小数组直接参与计算不会为它开一份大内存。所以「广播省内存」是真的但反过来如果广播的结果本身很大结果数组还是要占那么多内存——这一步没有省。五、reshape与-1numpy.reshape(a, shape)的签名是reshape(a, /, shape, orderC, *, copyNone)注意a是仅位置参数3.8 起支持这种语法shape既可以是整数也可以是一个元组。它和ndarray.reshape(...)方法功能一致区别只在「函数式」和「方法式」的写法。核心规则是官方那句能靠修改跨距完成变形时就返回视图否则需要复制。所以reshape的结果是视图还是副本取决于你的数组当时是不是连续、想要的形状能不能用跨距表达。-1是reshape里特别有用的写法它表示「这个维度的长度让 NumPy 自己推」。因为元素总数是固定的只要其他维度都定好了剩下的那一维可以算出来。# 适用于 Python 3.8 且已安装 NumPy以官方文档为准import numpy as npa np.arange(12)print(a.reshape(3, -1).shape) # (3, 4)12 除以 3 得 4print(a.reshape(-1, 4).shape) # (3, 4)print(a.reshape(3, 4).shape) # (3, 4)几条约束要记住一次只能出现一个-1两个未知数没法解而且元素总数必须能被整除np.arange(12).reshape(5, -1)会直接抛ValueError因为 12 不能被 5 整除。常见坑点❌ 把切片赋给变量后修改它以为改的是副本。✅ 基本索引和切片返回的是视图改它会改原数组需要独立数据就.copy()。❌ 用a.size去估算数组占了多少内存。✅size是元素个数字节数看nbytes等于size × itemsize。❌ 认为astype在类型相同时不会复制于是拿来「清一下类型」。✅astype默认copyTrue总是返回新数组类型相同时也会复制。❌ 用a[a 3]拿到结果后修改期待原数组跟着变。✅ 布尔索引和花式索引返回的都是副本改它不影响原数组。❌ 用flatten()期望拿到视图以省内存。✅flatten总是返回副本希望尽量是视图用ravel但它无法保持连续时仍会复制。❌ 在reshape里同时写两个-1。✅ 一次只能有一个-1其余各维必须写全且元素总数必须能被整除否则抛ValueError。❌ 认为「广播会把小数组复制成大数组」因而担心内存。✅ 广播不会真的复制小数组它按原值参与计算但结果数组本身仍要按结果形状分配内存。❌ 用「行数、列数」去理解所有数组的shape。✅shape是一个由各轴长度组成的元组维数不限于是 2用「第 0 轴、第 1 轴」的说法更准确。总结概念一句话理解最容易踩的地方ndarray结构数据缓冲区 元数据把它当成普通容器shape各轴长度的元组只用「行列」去理解dtype元素类型决定itemsize用原地改dtype重新解读数据size/nbytes元素个数 / 字节数两者混用strides沿各轴前进一格要跳多少字节忽略「转置后不连续」的后果视图 vs 副本切片是视图花式/布尔索引是副本改视图后原数组跟着变astype默认总是复制大数组上循环转换广播从最右轴比较相等或为 1 即兼容以为会复制小数组reshape/-1能改跨距就是视图-1表示自动推算一维写两个-1、总数除不尽把这一篇的概念压缩成一句话ndarray是「数据 描述数据的元数据」你平时改的大多数都是元数据而真正改数据的操作会以视图或副本的形式表现出来。抓住strides和base这两个抓手再去理解视图、广播、reshape就都有了统一的解释。参考ndarray的属性、跨距与内部组织、视图与副本的判定、广播规则、reshape与astype的语义均以 NumPy 官方文档的arrays.ndarray、「Copies and views」、「Broadcasting」以及numpy.reshape、numpy.ndarray.astype章节为准NumPy 为第三方库需pip install numpy本文代码未在本机运行仅作人工推演。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →