1、内存设计与实现原理
ndarray的内存结构
类ndarray
的实例本质上由一个连续的一维内存段和一个索引方案组合而成。这种将所有数据存放在一个连续的一维内存段的存储方式,实际与C语言中的多维数组存储方式一致。但Numpy索引的灵活设计,使得ndarray
对象可适应于任何跨步索引方案,以下对ndarray
对象以行作为主要存储顺序的内存设计进行说明。
Numpy在创建数组或建立数组视图时,将数组的信息记录在不同属性,如shape属性指定各维度的元素的数量,dtype属性指定元素类型及其解释方式,strides属性指定各维度的跨度,itemsize属性指定单个元素占用字节数。

索引的解析
对于NNN维的数组array\sf arrayarray,将各维度的跨度存放在strides\sf{strides}strides列表中,则第k+1k+1k+1维的跨度
strides[k]={itemsize,k=N−1itemsize×∏j=k+1N−1shape[j],k=0,⋯ ,N−2{\sf strides}[k] = \begin{cases}
{\sf{itemsize}}, &k=N-1\\
{\sf{itemsize}} \times \prod_{j=k+1}^{N-1}{\sf{shape}}[j], &k=0,\cdots,N-2\\
\end{cases}strides[k]={itemsize,itemsize×∏j=k+1N−1shape[j],k=N−1k=0,⋯,N−2
令数组array\sf arrayarray的首元素地址为&array\&{\sf{array}}&array,则数组中坐标为(i0,i1,⋯ ,iN−1)(i_0,i_1,\cdots,i_{N-1})(i0,i1,⋯,iN−1)的元素的地址
&array[i0][i1]⋯[iN−1]=&array+∑k=0N−1ik×strides[k]\&{\sf{array}}[i_0][i_1]\cdots[i_{N-1}]= \&{\sf{array}} + \sum_{k=0}^{N-1}i_{k}\times {\sf{strides}}[k]&array[i0][i1]⋯[iN−1]=&array+k=0∑N−1ik×strides[k]
利用以上公式计算出给定坐标对应的地址,即&array[i0][i1]⋯[iN−1]\&{\sf{array}}[i_0][i_1]\cdots[i_{N-1}]&array[i0][i1]⋯[iN−1],然后即可得到array[i0][i1]⋯[iN−1]{\sf{array}}[i_0][i_1]\cdots[i_{N-1}]array[i0][i1]⋯[iN−1]的值。
实例
对于数据类型为int32(占4字节)
的二维整型数组
b=(0123456789101112131415)\bm b = \left(\begin{matrix}
0 &1 &2 &3 \\
4 &5 &6 &7 \\
8 &{\bm\color {red}9}&10 &11 \\
12 &13 &14 &15
\end{matrix}\right)b=⎝⎜⎜⎛0481215913261014371115⎠⎟⎟⎞
其形状shape=(4,4){\sf shape}=(4,4)shape=(4,4),itemsize=4{\sf itemsize}=4itemsize=4,因此不难计算出跨度列表strides=[16,4]{\sf strides}=[16, 4]strides=[16,4]。
若以b[2][1]b[2][1]b[2][1]的索引方式访问元素999,实际底层是将引用地址解析为
&b[2][1]=&b+2×strides[0]+1×strides[1]=&b+36\&b[2][1] = \&b + 2 \times {\sf strides}[0] + 1\times {\sf strides}[1] = \&b+36&b[2][1]=&b+2×strides[0]+1×strides[1]=&b+36
显然,地址&b+36\&b+36&b+36存储的值为目标元素999,即完成了索引的解析。
2、切片索引的视图原理
切片索引的解析
对于二维数组bbb,使用以下起始值及步长对其进行切片
a=b[begin0:end0:step0,begin1:end1:step1]a = b[{\sf begin_0:end_0:step_0}, {\sf begin_1:end_1:step_1}]a=b[begin0:end0:step0,begin1:end1:step1]
易知,结果数组aaa的首地址
&a=&b[begin0,begin1]=&b+begin0×strides[0]+begin1×strides[1]\&a=\&b[{\sf begin_0}, {\sf begin_1}]=\&b+{\sf begin_0}\times {\sf strides}[0] + {\sf begin_1}\times {\sf strides}[1]&a=&b[begin0,begin1]=&b+begin0×strides[0]+begin1×strides[1]
基于各维度的切片步长,更新切片索引结果数组aaa的跨度列表
strides′=step×strides=[step0,step1]×strides{\sf strides'} = {\sf step} \times {\sf strides} = [{\sf step_0}, {\sf step_1}] \times {\sf strides}strides′=step×strides=[step0,step1]×strides
此时,利用结果数组aaa的起始地址&a\&a&a以及新的跨度列表strides′{\sf strides'}strides′,使用索引a[i][j]a[i][j]a[i][j]的形式依然可以访问到目标元素。
切片索引要求每一维度具有固定的切片步长,因此我们仅需要创建一个原始数组的视图,并根据起始位置以及各维度的切片步长,修改视图的起始位置以及各维度的跨度,即可访问到结果数组中的目标元素。因此,切片索引不需要复制原始数据。由于整数数组索引步数的随机性,不能通过更改索引方案的方案访问原数组。因此,整数数组索引返回的是原始数组的副本。
实例
对于数组bbb,执行切片a=b[::3,1::2]a = b[::3, 1::2]a=b[::3,1::2],得到红色标记位置元素
b=(0123456789101112131415),a=(131315)\bm b = \left(\begin{matrix}
0 &{\bm\color {red}1} &2 &{\bm\color {red}3} \\
4 &5 &6 &7 \\
8 &9 &10 &11 \\
12 &{\bm\color {red}13} &14 &{\bm\color {red}15}
\end{matrix}\right), \quad\bm
a=\left(\begin{matrix}
1 &3\\
13 &15
\end{matrix}\right)b=⎝⎜⎜⎛0481215913261014371115⎠⎟⎟⎞,a=(113315)
原数组bbb的跨度列表strides=[16,4]{\sf strides}=[16, 4]strides=[16,4],结果数组aaa的首地址&a=&b+4\&a=\&b+4&a=&b+4,结果数组aaa的跨度列表strides′=[48,8]{\sf strides'}=[48, 8]strides′=[48,8]。
通过索引a[1,1]a[1,1]a[1,1]的方式访问元素15,实际底层将引用地址解析为
&a[1][1]=&a+1×strides′[0]+1×strides′[1]=&a+56=&b+60\&a[1][1] = \&a + 1 \times {\sf strides'}[0] + 1\times {\sf strides'}[1] = \&a+56 = \&b+60&a[1][1]=&a+1×strides′[0]+1×strides′[1]=&a+56=&b+60
此外
&b[3][3]=&b+3×strides[0]+3×strides[1]=&b+60\&b[3][3] = \&b + 3 \times {\sf strides}[0] + 3\times {\sf strides}[1] = \&b+60&b[3][3]=&b+3×strides[0]+3×strides[1]=&b+60
显然,使用切片索引仅通过更改索引方案,即可访问到原始数据。