6.2. ndarray¶
ndarray 是在 numpy 中持有數值資料的型別。它二合一:一塊單一且緊密排列的資料區塊,以及該區塊前方一個用來說明如何讀取它的小型描述符。
6.2.1. 盒子內部¶
這塊資料區塊首尾相連地存放陣列的每一個元素,元素之間沒有任何多餘的東西。每個元素佔用相同數量的位元組 —— uint8 值的陣列每個一位元組、uint16 每個兩位元組、float 每個四位元組。一個 256 元素的 uint8 陣列正好是 256 位元組的資料;同樣的 256 個數字放進 Python 的 list 則要佔一千位元組 —— 不管數值實際只需要幾個位元,每個元素都佔一個 32 位元的欄位。
描述符記錄該區塊代表的 意義。無論維度有多少,五個值就足以描述任何矩形陣列:
dtype—— 元素型別。決定每個元素佔用幾個位元組以及它能持有的數值範圍(於 Dtype 資料型別 中說明)。itemsize—— 一個元素的位元組寬度,由 dtype 推導而來。ndim—— 維度的數量(向量為 1、矩陣為 2、立體為 3,最多到 4)。shape—— 以 tuple 表示沿每個維度的大小。
就這樣。numpy 中每一條快速路徑 —— 算術運算、歸約、廣播、切片 —— 都直接從這五個值再加上資料指標運作,沒有任何逐元素的 Python 開銷。
6.2.2. 這項設計帶來什麼¶
「緊密排列的區塊 + 小型描述符」衍生出三項特性,並定義了本章其餘部分的行為方式。
逐元素數學運算以單次呼叫執行。 兩個形狀相符的陣列之間的 a + b 會把這兩個緩衝區相加並寫入第三個,全部都在一次函式庫呼叫之內完成。np.sin(a) 對每個元素的正弦也是如此。算術、比較與位元運算子全都以這種方式運作。
以不同方式檢視同一份資料是免費的。 要求陣列的某個子區域,或要求同一份資料以不同形狀呈現,都不會搬動任何位元組。這項操作會回傳一個指向 同一份 資料區塊的新描述符。其結果稱為 視圖(view) —— 通往同一底層緩衝區的第二個視窗。透過視圖寫入就是寫入來源。
形狀不同也照樣可行。 較短的陣列對上較長的、一列對上一個矩陣、一欄對上一列 —— numpy 會透過 廣播(broadcasting) 將它們對齊,這是一組決定哪個短軸要拉伸以匹配長軸的小規則。這種拉伸是虛擬的;不會複製任何資料。
6.2.3. 這項設計的代價¶
同一項設計衍生出兩項限制。
每個元素都有相同的型別。 一個 list 可以在一個 int 旁邊放一個 str,再旁邊放一個含有另外三個 int 值的 list;ndarray 則不行。dtype 在建構時即固定。Dtype 資料型別 頁面說明了 numpy 支援的那一小組型別,以及固定型別所衍生出的規則。
增長陣列並非免費。 list 會在尾端保留備用欄位,因此能廉價地支援 .append。ndarray 的大小恰好就是它所需要的大小;附加元素將意味著要配置一個新的、更大的緩衝區,並把舊內容複製進去。它刻意沒有 append() 方法。相機上的正確模式是預先以最終大小配置目的地,然後 填入 它;效能 說明了這項技巧。
以一個緊密排列的型別化緩衝區存放資料、一個小型描述符存放中介資料,再加上三項行為保證(快速的逐元素數學運算、對同一份資料的免複製替代視圖,以及可廣播的形狀),ndarray 便是本章其餘部分所依憑的基礎。一個陣列究竟如何產生 —— 從字面量、從預先填入的配置、從周邊裝置緩衝區 —— 則是下一個實務問題。