NumPy 数组的内部组织#

了解一些关于NumPy数组在内部是如何处理的有助于更好地理解NumPy.本节不会深入细节.希望了解全部细节的读者请参考Travis Oliphant的书 Guide to NumPy.

NumPy 数组由两个主要部分组成:原始数组数据(从现在起,称为数据缓冲区),以及关于原始数组数据的信息.数据缓冲区通常是人们认为的 C 或 Fortran 中的数组,一个包含固定大小数据项的 :term:`连续`(且固定)的内存块.NumPy 还包含一组描述如何解释数据缓冲区中数据的重要数据.这些额外信息包含(除其他外):

  1. 基本数据元素的字节大小.

  2. 数据缓冲区中数据的起始位置(相对于数据缓冲区开始位置的偏移量).

  3. 维度 的数量和每个维度的大小.

  4. 每个维度元素之间的分隔(步幅).这不一定是元素大小的倍数.

  5. 数据的字节顺序(可能不是本机的字节顺序).

  6. 缓冲区是否为只读.

  7. 关于基本数据元素解释的信息(通过 dtype 对象).基本数据元素可能简单如一个整数或浮点数,也可能是一个复合对象(例如,:term:类似结构的数据类型)、固定字符字段或Python对象指针.

  8. 数组是否应解释为 C-orderFortran-order.

这种安排允许非常灵活地使用数组.它允许的一件事是简单地更改元数据以改变对数组缓冲区的解释.更改数组的字节顺序是一个简单的更改,不涉及数据的重新排列.数组的 形状 可以很容易地改变,而不需要更改数据缓冲区中的任何内容或进行任何数据复制.

除此之外,还可以创建一个新的数组元数据对象,该对象使用相同的数据缓冲区来创建该数据缓冲区的新 视图 ,该视图对缓冲区有不同的解释(例如,不同的形状、偏移量、字节顺序、步幅等)但共享相同的数据字节.NumPy 中的许多操作正是这样做的,例如 切片 .其他操作,如转置,不会在数组中移动数据元素,而是改变形状和步幅的信息,以便数组的索引发生变化,但数组中的数据不会移动.

通常这些新版本的数组元数据但相同的数据缓冲区是数据缓冲区的新视图.有一个不同的 ndarray 对象,但它使用相同的数据缓冲区.这就是为什么如果确实需要创建一个新的独立副本,有必要通过使用 copy 方法强制复制的原因.

新的数组视图意味着数据缓冲区的对象引用计数增加.简单地消除原始数组对象不会移除数据缓冲区,如果其他视图仍然存在的话.

多维数组索引顺序问题#

参见

基础.索引

什么是索引多维数组的正确方法?在你急于得出关于索引多维数组的唯一正确方法的结论之前,了解为什么这是一个令人困惑的问题是值得的.本节将尝试详细解释NumPy索引的工作原理,以及为什么我们为图像采用这样的约定,以及何时采用其他约定可能是合适的.

首先要理解的是,对于索引二维数组有两种冲突的约定.矩阵表示法使用第一个索引来指示选择哪一行,使用第二个索引来指示选择哪一列.这与面向几何的图像约定相反,在图像中,人们通常认为第一个索引代表x位置(即列),第二个索引代表y位置(即行).这本身就是许多混乱的根源;面向矩阵的用户和面向图像的用户在索引方面期望两种不同的结果.

第二个需要理解的问题是如何索引对应于数组在内存中存储的顺序.在Fortran中,当遍历一个二维数组的元素时,第一个索引是变化最快的索引,因为它存储在内存中.如果你采用矩阵索引的约定,那么这意味着矩阵是按列存储的(因为第一个索引变化时会移动到下一行).因此,Fortran被认为是一种列优先的语言.C则有相反的约定.在C中,最后一个索引在遍历数组时变化最快.因此,C是一种行优先的语言.矩阵按行存储.注意,在这两种情况下,都假设使用了矩阵索引的约定,即对于Fortran和C,第一个索引是行.注意,这种约定意味着索引约定是不变的,而数据顺序变化以保持这一点.

但这并不是唯一的看法.假设一个人有大型的二维数组(图像或矩阵)存储在数据文件中.假设数据是按行而不是按列存储的.如果我们想要保留我们的索引约定(无论是矩阵还是图像),这意味着根据我们使用的语言,我们可能被迫在读入内存时重新排序数据以保留我们的索引约定.例如,如果我们不重新排序就读入按行排序的数据到内存中,它将匹配C语言的矩阵索引约定,但不匹配Fortran的.相反,它将匹配Fortran的图像索引约定,但不匹配C的.对于C语言,如果一个人使用按行存储的数据,并且想要保留图像索引约定,那么在读入内存时必须重新排序数据.

最终,你为 Fortran 或 C 所做的工作取决于哪个更重要,不是重新排序数据就是保留索引约定.对于大图像,重新排序数据可能是昂贵的,通常会反转索引约定以避免这种情况.

NumPy 的情况使这个问题变得更加复杂.NumPy 数组的内部机制足够灵活,可以接受任何索引顺序.通过操作内部的 步幅 信息,可以简单地重新排列索引,而根本不需要重新排列数据.NumPy 会知道如何将新的索引顺序映射到数据,而无需移动数据.

所以如果这是真的,为什么不选择与你最期望的索引顺序相匹配的顺序呢?特别是,为什么不定义行排序的图像以使用图像约定呢?(这有时被称为Fortran约定与C约定,因此NumPy中数组排序的’C’和’FORTRAN’顺序选项.)这样做的一个缺点是潜在的性能损失.通常会按顺序访问数据,无论是隐式地在数组操作中还是通过显式地循环遍历图像的行.当这样做时,数据将以非最佳顺序访问.随着第一个索引的增加,实际上发生的是在内存中相距很远的元素被顺序访问,通常内存访问速度较慢.例如,对于一个二维图像``im``,定义为``im[0, 10]``表示``x = 0``,``y = 10``处的值.为了与通常的Python行为一致,``im[0]``将表示``x = 0``处的列.然而,由于数据以行顺序存储,这些数据将分布在整个数组中.尽管NumPy的索引很灵活,但它无法掩盖基本操作因数据顺序而变得低效的事实,或者获取连续子数组仍然很尴尬(例如,``im[:, 0]``表示第一行,而``im[0]``不是).因此,不能使用诸如``im``的行;``im``的列这样的惯用法;虽然``im``的列有效,但不会产生连续的列数据.

事实证明,NumPy 在处理 ufuncs 时足够智能,能够确定内存中变化最快的索引,并将其用于最内层的循环.因此,对于 ufuncs 来说,在大多数情况下,两种方法没有本质上的优势.另一方面,使用 ndarray.flat 与 FORTRAN 顺序数组会导致非最佳内存访问,因为在展平数组(实际上是迭代器)中的相邻元素在内存中不是连续的.

确实,事实是Python在列表和其他序列上的索引自然会导致从外到内的顺序(第一个索引获取最大的分组,下一个获取次大的分组,最后一个获取最小的元素).由于图像数据通常按行存储,这对应于行内的位置是最后索引的项目.

如果你确实想使用 Fortran 排序,请意识到有两种方法需要考虑:1) 接受第一个索引在内存中不是变化最快的,并且让你的所有 I/O 例程在从内存到磁盘或反之亦然时重新排序你的数据,或者使用 NumPy 的机制将第一个索引映射到变化最快的数据.如果可能的话,我们推荐前者.后者的缺点是,除非你小心使用 order 关键字,否则许多 NumPy 的函数会生成没有 Fortran 排序的数组.这样做会非常不方便.

否则,我们建议在访问数组元素时简单地学习反转通常的索引顺序.诚然,这有悖常理,但它更符合 Python 语义和数据的自然顺序.