NumPy 数组的内部组织#

它有助于了解 NumPy 数组的幕后处理方式,从而更好地理解 NumPy。本节不会详细介绍。希望了解完整详细信息的人请参阅 Travis Oliphant 的书Guide to NumPy

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

  1. 基本数据元素的大小(以字节为单位)。

  2. 数据缓冲区内数据的开头(相对于数据缓冲区开头的偏移量)。

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

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

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

  6. 缓冲区是否是只读的。

  7. dtype有关基本数据元素解释的信息(通过对象)。基本数据元素可以像 int 或 float 一样简单,也可以是复合对象(例如 struct-like)、固定字符字段或 Python 对象指针。

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

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

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

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

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

多维数组索引顺序问题#

也可以看看

ndarray 上的索引

索引多维数组的正确方法是什么?在得出关于索引多维数组的唯一正确方法的结论之前,有必要了解为什么这是一个令人困惑的问题。本节将尝试详细解释 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定义的二维图像,表示、处的值。为了与通常的 Python 行为保持一致,将表示 处的一列。然而,由于数据按行顺序存储,因此该数据将分布在整个数组中。尽管 NumPy 的索引具有灵活性,但它并不能真正掩盖这样的事实:由于数据顺序或获取连续子数组仍然很困难(例如,对于第一行, vs ),基本操作变得低效 。因此,不能使用诸如 for row in 之类的习语; for col in确实有效,但不会产生连续的列数据。im[0, 10]x = 0y = 10im[0]x = 0im[:, 0]im[0]imim

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

事实上,Python 对列表和其他序列的索引自然会导致从外到内的排序(第一个索引获得最大的分组,第二大的分组,最后一个索引获得最小的元素)。由于图像数据通常存储在行中,因此这对应于行中最后一个索引项的位置。

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

否则,我们建议在访问数组元素时简单地学习反转索引的通常顺序。诚然,它违背了规律,但它更符合 Python 语义和数据的自然顺序。