6.6. 索引與切片

ndarray 有四種定址方式:單一索引、切片、布林遮罩,以及各自的指派形式。

6.6.1. 單一元素

方括號索引會傳回指定位置上的值:

a = np.arange(10, dtype=np.uint8)
print(a[0], a[-1])      # 0 9
print(a[1], a[-2])      # 1 8

負索引從尾端往前數,與 Python list 相同。超出範圍的索引會引發 IndexError

對於較高階陣列,每個軸都要有一個索引。這些索引放在同一組括號內,以逗號分隔:

m = np.arange(9, dtype=np.uint8).reshape((3, 3))
print(m[1, 1])          # 4
print(m[2, 0])          # 6

當提供的索引數少於軸數時,未被索引的軸會保持完整。結果是來源的 降階視圖:

print(m[0])             # the first row, as a 1-D view of m

6.6.2. 切片

切片 start:stop:step 會傳回陣列的 視圖。視圖與來源共享底層資料緩衝區;透過視圖寫入即會寫入來源:

a = np.arange(10, dtype=np.uint8)
v = a[::2]              # array([0, 2, 4, 6, 8], dtype=uint8)
v[0] = 99
print(a)
# array([99, 1, 2, 3, 4, 5, 6, 7, 8, 9], dtype=uint8)

當需要一個獨立緩衝區時,copy() 會明確產生一個。

切片可自然地延伸到更高維度。每個軸都有自己的切片:

m = np.array([[1, 2, 3],
              [4, 5, 6],
              [7, 8, 9]], dtype=np.uint8)

m[0]            # first row
m[0, :2]        # first two elements of row 0
m[:, 0]         # column 0 (still 2-D in ulab)
m[-1]           # last row
m[::2, ::2]     # every other row, every other column

混用整數(單一索引,會捨棄該軸)和切片(會保留該軸)是允許的,這也是通常撰寫單列/單行存取的方式。

6.6.3. 布林遮罩

與來源形狀相同的布林陣列會選取遮罩為 True 的元素。布林索引目前適用於 1 維陣列;較高階的輸入會引發 NotImplementedError:

a = np.arange(9, dtype=np.float)
mask = a < 5
print(a[mask])

輸出:

array([0.0, 1.0, 2.0, 3.0, 4.0], dtype=float)

遮罩是一個普通的 bool ndarray,因此任何產生它的運算式都可運作:

b = np.array([4, 4, 4, 3, 3, 3, 13, 13, 13], dtype=np.uint8)
a = np.arange(9, dtype=np.uint8)
print(a[a * a > np.sin(b) * 100.0])

布林索引會傳回一份 複本。被選取的元素位於遮罩為 True 的任意位置——而非沿著來源的固定間隔——因此沒有任何描述子可供視圖用來定址它們,結果會具現化到它自己的緩衝區中。

6.6.4. 整數陣列索引

在括號中傳入一個索引串列或陣列,即可一步挑出那些元素:

a = np.array([10, 20, 30, 40, 50], dtype=np.uint8)
a[[0, 2, 4]]
# array([10, 30, 50], dtype=uint8)

結果是一份 複本;被挑出的元素不再與來源共享儲存空間。同樣的形式也可用於指派的左側:

a[[0, 2, 4]] = 0
# array([0, 20, 0, 40, 0], dtype=uint8)

take()(在 選取與重新排列 中介紹)是相同運算的函式形式,並接受 out= 關鍵字以在串流迴圈中達成無配置使用。

6.6.5. 切片指派

切片與遮罩可出現在指派的 左側,也可出現在右側。右側可以是純量、另一個陣列,或一個視圖:

m = np.zeros((3, 3), dtype=np.uint8)
m[0]      = 1            # whole row 0 set to 1
m[:, 2]   = 3            # whole column 2 set to 3
m[1, 1:3] = [7, 8]       # row 1, columns 1 and 2

位於左側的布林遮罩會替換滿足條件的元素:

a = np.arange(9, dtype=np.uint8)
a[a < 3] = 99
# array([99, 99, 99, 3, 4, 5, 6, 7, 8], dtype=uint8)

a = np.arange(9, dtype=np.uint8)
b = np.array(range(9)) + 12
a[b < 15] = b[b < 15]
# array([12, 13, 14, 3, 4, 5, 6, 7, 8], dtype=uint8)

6.6.6. 為何切片指派在相機上很重要

切片指派是透過一個已經存在的陣列進行寫入。不會配置新陣列。這就是以下兩者的差別:

out = a + b              # makes a new array the size of a
out = out * 2            # makes another new array

以及:

out[:] = a               # writes into the existing out
out   += b               # in place
out   *= 2               # in place

第一種版本向相機要求兩個全新陣列大小的 RAM;第二種版本則什麼都不要求。在 RAM 有限的微控制器上,這個差別往往就是一個能順暢執行的指令碼與一個會耗盡記憶體的指令碼之間的差別。

效能 詳細介紹了這個模式。目前最重要的規則是:切片指派、就地算術運算子(+=*=、……)以及通用函式上的 out= 關鍵字,是讓無配置更新得以成真的三項工具。