NumPy 陣列的內部組織#

了解一些關於 NumPy 陣列在底層如何處理的知識,有助於更好地理解 NumPy。本節不會深入探討細節。希望了解完整細節的人員,請參考 Travis Oliphant 的著作 NumPy 指南

NumPy 陣列由兩個主要組件組成:原始陣列資料(以下稱為資料緩衝區),以及關於原始陣列資料的資訊。資料緩衝區通常是人們在 C 或 Fortran 中認為的陣列,一個連續的(且固定的)記憶體區塊,包含固定大小的資料項目。NumPy 還包含大量資料,描述如何解釋資料緩衝區中的資料。這些額外資訊包含(以及其他事項)

  1. 基本資料元素的大小(以位元組為單位)。

  2. 資料在資料緩衝區內的起始位置(相對於資料緩衝區起始位置的偏移量)。

  3. 維度的數量以及每個維度的大小。

  4. 每個維度中元素之間的間隔(步幅)。這不一定是元素大小的倍數。

  5. 資料的位元組順序(可能不是原生位元組順序)。

  6. 緩衝區是否為唯讀。

  7. 關於基本資料元素解釋的資訊(透過 dtype 物件)。基本資料元素可能像 int 或 float 一樣簡單,或者它可能是複合物件(例如,類結構)、固定字元欄位或 Python 物件指標。

  8. 陣列是否要解釋為 C 順序Fortran 順序

這種安排允許非常彈性地使用陣列。它允許的一件事是簡單地更改元資料,以更改陣列緩衝區的解釋。更改陣列的位元組順序是一個簡單的更改,不涉及資料的重新排列。形狀 可以非常容易地更改陣列的形狀,而無需更改資料緩衝區中的任何內容或任何資料複製。

在其他可能的事情中,可以建立一個新的陣列元資料物件,該物件使用相同的資料緩衝區來建立該資料緩衝區的新 視圖,該視圖對緩衝區具有不同的解釋(例如,不同的形狀、偏移量、位元組順序、步幅等),但共享相同的資料位元組。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,其定義使得 im[0, 10] 表示 x = 0y = 10 的值。為了與通常的 Python 行為一致,那麼 im[0] 將表示 x = 0 處的列。然而,該資料將分佈在整個陣列中,因為資料是以行順序儲存的。儘管 NumPy 的索引具有彈性,但它實際上無法掩蓋基本操作因資料順序而變得效率低下,或者取得連續子陣列仍然很笨拙的事實(例如,im[:, 0] 代表第一行,而不是 im[0])。因此,人們無法使用諸如 for row in im 之類的慣用語;for col in im 可以運作,但不會產生連續的列資料。

事實證明,NumPy 在處理 ufuncs 時足夠聰明,可以確定哪個索引在記憶體中變化最快,並將其用於最內層迴圈。因此,對於 ufuncs,在大多數情況下,這兩種方法都沒有很大的內在優勢。另一方面,使用 ndarray.flat 與 FORTRAN 排序的陣列將導致非最佳的記憶體存取,因為展平陣列(實際上是迭代器)中的相鄰元素在記憶體中不是連續的。

實際上,事實是 Python 在列表和其他序列上的索引自然而然地導致從外到內的排序(第一個索引取得最大的分組,下一個索引取得次大的分組,最後一個索引取得最小的元素)。由於影像資料通常按行儲存,因此這對應於行內的位置是最後一個索引的項目。

如果您確實想要使用 Fortran 排序,請意識到有兩種方法需要考慮:1) 接受第一個索引只是記憶體中變化最快的索引,並讓您的所有 I/O 常式在從記憶體到磁碟或反之亦然時重新排序您的資料,或者使用 NumPy 的機制將第一個索引對應到變化最快的資料。如果可能,我們建議前者。後者的缺點是 NumPy 的許多函數會產生沒有 Fortran 排序的陣列,除非您小心使用 order 關鍵字。這樣做會非常不方便。

否則,我們建議簡單地學習在存取陣列元素時反轉通常的索引順序。誠然,這與慣例相悖,但它更符合 Python 語意和資料的自然順序。