前面的内容中简单介绍了多维数组ndarry的创建、基本属性以及索引、切片的常用操作。但是,要真正更好地理解各项操作的使用及预期操作的结果时,还是需要对ndarray的内部机理及所谓的数据存储顺序有一定的了解,才能将ndarry应用得更加得心应手。
本文的主要内容有:
1、ndarray对象的内部机理
2、ndarray的数据存储顺序
ndarray对象的内部机理NumPy中的多维数组结构ndarray之所以快,是因为ndarray提供了一种将同构类型的数据块(可以是连续的或者步进式的)解释为多维数组对象的方式。数据类型(dtype)决定了数据的解释方式,比如浮点型、整型、布尔型等。
我们对ndarray的一些操作,比如切片,实质上只是创建了一个针对原始数据存储的新的视图,更确切说,是创建了一种原始数据的元数据,从而产生对原始数据的新的解释方式。
文字内容比较抽象,首先通过一个图像来粗糙解释ndarray对象的内部结构:
从上图中可以看出,ndarray对象主要有两个部分组成:
1、原始数组数据,也就是数据缓冲区,可以理解成为C数组或者Fortran数组,是一个连续且固定的内存块,连续存储真实的数组元素。
2、元数据,有关原始数组数据的各种信息,用于描述如何解释数据缓冲区中的数据。
正是由于元数据和原始数组数据分别存储结构,可以支撑后续零拷贝的数组视图的实现。也就是说,当我们对原始数组进行切片时,实际上是创建了一个新的元数据信息,增加了一种对原始数组的数据的新的解释方式,而实际上还是共用同一份原始数组数据。
可以通过代码来验证上述内容,首先先看一个ndarray的内部结构信息:
简单说明一下:
1、ctypes.data属性:存储了原始数组的指针
2、dtype属性:数组元素的类型
3、itemsize属性:数组元素占用的字节数
4、shape属性:数组的形状
5、strides属性:步进信息
其中,strides属性需要特别说明一下,所谓步进信息,也就是一个步长的数组,记录了在各个维度上,前进到下一个元素所需要移动的字节数。
接下来,我们创建数组的切片,同时验证是否创建新的元数据,以及是否指向了原始数组:
结合上面的代码执行结果,简单说明一下NumPy中ndarray的所谓“0拷贝”机制。
1、所谓0拷贝,实际上还是会创建一个Python对象,0拷贝描述的是不创建新的原始数组,ctypes.data指针指向的是原始数组中切片中的首个元素的位置。
2、切片对象存储的步进信息strides是真正用于解释切片中可见数据元素的元信息。
ndarray的数据存储顺序了解了ndarray对象的内部机理,接下来再来对原始数据的存储顺序进行展开说明。
关于数据的存储,其实主要有两种存储风格:
1、C风格:也就是C语言中,数组数据的存储顺序,也叫作Row-Major(行优先、按行存储)的数据存储顺序。
2、Fortran风格:也就是Fortran语言中,数组数据的存储顺序,也叫作Column-Major(列优先、按列存储)的数据存储顺序。
首先,通过代码来看下这两种存储顺序(更确切的说是数据的访问顺序,因为底层存储是数组创建之初已经确定了)
可以看到,如下图所示,对同一个数组创建两种顺序的解释视图后,对应的步进信息有明显的不同,索引元素的顺序确实发生了变化。
但是,我们需要知道,在“0拷贝”机制之下,原始数组的数据存储顺序是没有发生任何修改的。
同样通过代码来说明:
简单说明一下上述代码:
1、首先获取到数组的数据类型,从而确认每个数组元素所占用的字节数。
2、通过ctypes类库将ndarray中指向原始数组的指针属性ctypes.data转换为一个对应数据类型的C指针。
3、通过C指针对原始数组进行元素的索引,从而确认存储顺序未发生变化。
总结本文首先介绍了ndarray对象的内部机理,理解了其元数据和原始数组的构成,并引出了0拷贝视图机制的基础strides步进属性。最后介绍了关于数组数据存储顺序的两种方式,并通过C指针验证了不同视图下的原始数组存储顺序不变的特性。
以上就是本文的全部内容,感谢您的拨冗阅读!