NumPy 数组的内部组织#
了解 NumPy 数组在底层是如何处理的,有助于更好地理解 NumPy. 本节提供一个简要的解释. 更多细节可以在 Travis Oliphant 的书 Guide to NumPy 中找到.
NumPy 数组由两个主要组成部分组成:原始数组数据(以下简称数据缓冲区)以及关于原始数组数据的信息. 数据缓冲区通常是人们在 C 或 Fortran 中所认为的数组,它是包含固定大小数据项的 contiguous (和固定)的内存块. NumPy 还包含大量数据,用于描述如何解释数据缓冲区中的数据. 这些额外信息包含(除其他外):
基本数据元素的大小(以字节为单位).
数据在数据缓冲区中的起始位置(相对于数据缓冲区起点的偏移量).
dimensions 的数量和每个维度的大小.
每个维度的元素之间的间隔( stride ). 这不一定是元素大小的倍数.
数据的字节顺序(可能不是本机字节顺序).
缓冲区是否为只读.
关于基本数据元素解释的信息(通过
dtype对象). 基本数据元素可以像 int 或 float 一样简单,也可以是一个复合对象(例如, struct-like ),一个固定字符字段或 Python 对象指针.数组是否被解释为 C-order 或 Fortran-order .
这种安排允许非常灵活地使用数组. 它允许的一件事是简单地更改元数据以更改数组缓冲区的解释. 更改数组的字节顺序是一个简单的更改,不涉及数据的重新排列. 可以在不更改数据缓冲区中的任何内容或任何数据复制的情况下非常容易地更改数组的 shape .
Among other things that are made possible is one can create a new array metadata object that uses the same data buffer to create a new view of that data buffer that has a different interpretation of the buffer (e.g., different shape, offset, byte order, strides, etc) but shares the same data bytes. Many operations in NumPy do just this such as slicing. Other operations, such as transpose, don’t move data elements around in the array, but rather change the information about the shape and strides so that the indexing of the array changes, but the data in the array doesn’t move.
通常,这些新版本的数组元数据但相同的数据缓冲区是对数据缓冲区的新视图. 有一个不同的 ndarray 对象,但它使用相同的数据缓冲区. 这就是为什么如果真的想制作数据缓冲区的新且独立的副本,则必须通过使用 copy 方法来强制复制.
对数组的新查看方式意味着数据缓冲区的对象引用计数会增加.简单地去除原始数组对象不会删除数据缓冲区,如果该缓冲区的其他视图仍然存在.
多维数组索引顺序问题#
索引多维数组的正确方法是什么?在对索引多维数组的唯一正确方法得出结论之前,有必要了解为什么这是一个令人困惑的问题.本节将尝试详细解释 NumPy 索引的工作方式,以及为什么我们对图像采用我们采用的约定,以及何时可能适合采用其他约定.
首先要理解的是,对于二维数组的索引存在两种相互冲突的约定.矩阵表示法使用第一个索引来指示选择哪一行,第二个索引来指示选择哪一列.这与面向几何的图像约定相反,在图像中,人们通常认为第一个索引代表 x 位置(即列),第二个索引代表 y 位置(即行).这本身就是造成很多困惑的根源;面向矩阵的用户和面向图像的用户对于索引的期望是不同的.
第二个需要理解的问题是索引如何对应于数组在内存中存储的顺序.在 Fortran 中,当通过二维数组的元素在内存中存储时,第一个索引是变化最快的索引.如果您采用矩阵约定进行索引,那么这意味着矩阵一次存储一列(因为第一个索引在更改时移动到下一行).因此,Fortran 被认为是一种列优先语言.C 语言的约定正好相反.在 C 语言中,当通过数组在内存中存储时,最后一个索引变化最快.因此,C 语言是一种行优先语言.矩阵按行存储.请注意,在这两种情况下,它都假定矩阵索引约定正在使用,即对于 Fortran 和 C 语言,第一个索引都是行.请注意,此约定意味着索引约定是不变的,并且数据顺序会发生变化以保持不变.
但这并不是唯一的观察方式.假设一个大型二维数组(图像或矩阵)存储在数据文件中.假设数据是按行而不是按列存储的.如果我们要保留我们的索引约定(无论是矩阵还是图像),那就意味着,根据我们使用的语言,如果将数据读入内存以保留我们的索引约定,我们可能被迫重新排序数据.例如,如果我们在不重新排序的情况下将按行排序的数据读入内存,它将匹配 C 语言的矩阵索引约定,但不匹配 Fortran 语言.相反,它将匹配 Fortran 语言的图像索引约定,但不匹配 C 语言.对于 C 语言,如果一个人使用按行顺序存储的数据,并且想要保留图像索引约定,则必须在读入内存时重新排序数据.
最后,你对 Fortran 或 C 语言所做的事情取决于哪个更重要,是不重新排序数据还是保留索引约定.对于大型图像,重新排序数据可能代价高昂,并且通常会反转索引约定以避免这种情况.
NumPy 中的情况使这个问题更加复杂.NumPy 数组的内部机制足够灵活,可以接受任何索引顺序.人们可以通过操作数组的内部 stride 信息来简单地重新排序索引,而无需重新排序数据.NumPy 将知道如何将新的索引顺序映射到数据,而无需移动数据.
所以如果这是真的,为什么不选择最符合你期望的索引顺序呢?特别是,为什么不定义行排序图像以使用图像约定呢?(这有时被称为 Fortran 约定与 C 约定,因此 NumPy 中数组排序的"C"和"FORTRAN"顺序选项.)这样做的一个缺点是潜在的性能损失.通常按顺序访问数据,或者在数组操作中隐式地访问数据,或者通过循环图像的行来显式地访问数据.当这样做时,数据将以非最佳顺序访问.随着第一个索引的递增,实际上发生的是,在内存中相隔很远的元素被顺序访问,通常内存访问速度很差.例如,对于二维图像 im 的定义,使得 im[0, 10] 表示 x = 0 , y = 10 处的值.为了与通常的 Python 行为保持一致,那么 im[0] 将表示 x = 0 处的一列.然而,这些数据将分布在整个数组中,因为数据是以行顺序存储的.尽管 NumPy 索引的灵活性,但由于数据顺序或获得连续子数组仍然很尴尬(例如, im[:, 0] 表示第一行,而 im[0] ),它无法真正掩盖基本操作由于数据顺序而变得效率低下这一事实.因此,人们无法使用诸如 for row in im 之类的成语;for col in im 可以工作,但不会产生连续的列数据.
事实证明,当处理 ufuncs 时,NumPy 足够智能,可以确定哪个索引在内存中变化最快,并将其用于最内层循环.因此,对于 ufuncs,在大多数情况下,这两种方法都没有很大的内在优势.另一方面,将 ndarray.flat 与 FORTRAN 有序数组一起使用会导致非最佳内存访问,因为展平数组(实际上是迭代器)中的相邻元素在内存中不是连续的.
事实上,列表和其他序列上的 Python 索引自然会导致从外到内的排序(第一个索引获得最大的分组,下一个最大,最后一个获得最小的元素).由于图像数据通常存储在行中,因此这对应于行内的位置是最后一个被索引的项目.
如果您确实想使用 Fortran 排序,请注意有两种方法需要考虑:1) 接受第一个索引不是内存中变化最快的索引,并且让所有 I/O 例程在从内存到磁盘或反之亦然时重新排序您的数据,或者使用 NumPy 的机制将第一个索引映射到变化最快的数据.如果可能,我们建议前者.后者的缺点是,除非您注意使用 order 关键字,否则 NumPy 的许多函数都会生成没有 Fortran 排序的数组.这样做会非常不方便.
否则,我们建议您在访问数组元素时,只需学习反转通常的索引顺序.诚然,这与常理相悖,但它更符合 Python 语义和数据的自然顺序.