目前,NumPy的陣列提供了與Python內建的list相同的索引方式,這一方面,是為了幫助Python開發者,使其容易切入NumPy陣列的使用,另一方面,是因為總有必須以索引存取元素的需求。

然而,在可能的情況下,開發者仍應逐步從list的使用習慣,轉變為NumPy陣列的處理風格。

這類轉換的出發點是,避免以個別索引方式取出個別元素,因為個別索引時,NumPy陣列甚至會比Python的list來得慢,開發者應試著觀察陣列元素是否能畫分為連續範圍來處理,舉例來說,若a是NumPy陣列,採取a[2:3]這類指定索引範圍的操作,而對於大範圍的操作而言,就有機會比list來得快速。

這跟底層的實作有關,例如,NumPy陣列有View的概念,指定索引範圍的操作所傳回的NumPy陣列,並不會逐一複製元素參考,而會是針對原陣列的一個觀點,修改新陣列,同時,來源陣列也會被修改。

純Python中會使用巢狀list,來建立多維陣列,若a是個Python二維陣列,此時,我們可以用a[0][1:4]來取得第0列的索引1到3之元素,若a是NumPy二維陣列,雖然也可以使用同樣的方式,不過,我建議改用a[0,1:4]的寫法,結果雖然相同,卻會有較好的效能。

這是因為a[0,1:4]並不是a[0][1:4]的簡便寫法。a[0][1:4]是取得a[0]後,再依索引範圍取得結果陣列,a[0,1:4]則是計算軸0方向的索引0,與軸1方向的索引範圍1到3交叉的部份,也就是計算出叉積的結果。

因此,a[0][1]雖然也可以寫為a[0,1],兩者結果相同,意義卻不同,a[0,1]是指軸0方向索引0與軸1方向索引1的叉積。逗號分隔的寫法可以推廣至更多維的陣列,每個逗號間的範圍寫法,與list相同,至於逗號間的順序,是依軸0、1、2等來安排,類似地,a[0,1]的範圍寫法會建立View,而不是逐一複製元素參考。

強大的索引陣列

NumPy的陣列不單只能以數字作為索引,還可以接受陣列作為索引。

例如,若a是個NumPy陣列,a會一次取得a中索引0、1、4的元素,建立新NumPy陣列之後傳回,因為並非連續範圍,所以,這種方式不會建立View,修改傳回的陣列,並不會對原本的陣列造成影響。

進一步地,索引陣列也可以是布林值組成,例如若a長度為3,a傳回的陣列,只會包含True對應的索引元素,也就是說,這可以用來實作濾元素之類的任務,例如a[a>3],取得的陣列只會包含a中元素值大於3的部份。

這也是NumPy本身沒有內建filter這類函式的原因,有趣的是,NumPy中倒是有個where函式,例如,就a[a>3]而言,也可以寫為a[where(a>3)],嗯?何必多此一舉加個where呢?因為,where函式真正的作用,其實並不是過濾元素,而是依指定條件轉換元素值。

例如,若a包含元素1,2,4,5,where(a>3,a,a*10)會將a中大於3的元素保留不變,而小於等於3的部份乘以10,結果陣列會包含10,20,4,5。

道理很簡單,where的第一個參數接受布林陣列,如果元素是True,會選擇第二個參數指定陣列對應位置的值,否則會選擇第三個參數指定陣列對應位置的值。

如果只指定where的第一個參數,就只會傳回True元素的索引數字,也就是where(a>3)結果會是[2,3],a[where(a>3)]就等於a,也就相當於過濾出a>3的部份而得到[4,5]。

指定多維索引陣列

無論是指定使用數字或是布林值,索引陣列的指定都可以應用至更高的維度,在多個索引陣列之間使用逗號區隔,表示軸的分隔。

例如,若有個二維陣列a,我們想取得索引a[0][0]、a[1][3]、a[4][4],在軸0方向上,各索引須為[0,1,4],而在軸1方向上,各索引須為[0,3,4],也就是,以a的方式來指定。

由於方才談到的範圍指定方式,像是a[0,1:4],也是以逗號來區隔,為了避免混淆,指定多個索引陣列時,應該使用括號,也就是建立tuple來指定,例如a[([0,1,4],[0,3,4])],更多維度時,例如三維陣列,就是以a[(軸0索引陣列,軸1索引陣列,軸2索引陣列)]來指定。

無論維度為何,有兩個以上的索引陣列時,最後取得的元素,我們可以想成這些索引陣列zip起來的結果,因此,a會是一維陣列;若想類似範圍指定,以索引陣列的叉積方式來取得元素呢?這個時候,我們可以透過NumPy的ix_函式完成,ix這名稱就代表著索引的叉積(cross product),例如a[ix_([0,1,4],[0,3,4])],結果會是二維陣列。

若是多維的布林值索引陣列,我們可以想成zip後and運算的結果必須為True,例如,a[([True,True,False],[True,False,True])],預計取得的元素本是索引(0,0)、(1,1)、(2,2),然而,zip後成對布林值and運算的結果是True、False、False,最後的陣列中,就只會包含(0,0)之元素,也就是只有一個元素的一維陣列。

若想以([True,True,False],[True,False,True])建立真值表(Truth table)呢?我們同樣可以透過ix_函式來處理,例如,a[ix_([True,True,False],[True,False,True])],真值表中交叉的部份會取出,結果會是個二維陣列。

實際上,若不透過ix_,我們也可以自行透過陣列索引指定,得到叉積的結果。

因為基本上,索引陣列的形狀,決定了輸出陣列的形狀,如果索引陣列是[0,1,4],形狀就是(3,),而最後輸出的陣列形狀會是(3,),如果索引陣列是,形狀是(1,3),最後輸出陣列形狀,也會是(1,3)。

想自行透過陣列索引指定,得到叉積結果的話,事實上,會有一定的複雜度,而ix_函式目的就在於封裝這些複雜性。

若有興趣,你可以看看ix_的傳回值,其實,就是個複雜的索引陣列,如果對其傳回值有興趣理解的話,我們可以進一步參考〈NumPy 廣播機制〉中的說明。

list或NumPy陣列?

簡單來說,如果使用了NumPy陣列,在試著取元素進行處理時,應避免個別的索引指定。

可以的話,我們不妨先透過NumPy內建的Universal函式,或者,將純Python函式向量化,之後再來處理。

若真的必須面對索引時,可試著以範圍、索引陣列的方式,整批取得元素,以善用底層實作的高速機制。

也就是說,並不是我們使用了NumPy陣列,處理速度就會飛起來。想獲得效能上的改善,開發者必須改變思考方式。

事實上,如果程式中經常以個別方式對NumPy陣列指定索引,效能反而會比純list來得差,這時考量使用純list,或者將NumPy陣列透過tolist方法轉為list,再來個別指定索引,如此反而會有更好的存取效能。

專欄作者

熱門新聞

Advertisement