想使用Python來處理大量資料,往往會建議使用NumPy,只不過一直以來,我僅將NumPy當成是純粹的程式庫。

它有許多基於陣列的操作,然而,我也只當成更方便的list版本,寫程式時,能直接使用NumPy陣列處理,就以陣列來處理,如果不行時,就透過迴圈處理,而這樣漫不經心又缺乏系統性的消化與整理,很容易沒多久就將NumPy的使用方式,忘得一乾二淨。

由於先前一段日子我都在玩電腦繪圖相關的東西,像是OpenSCAD、p5.js,當繪圖玩到某個程度之後,結合圖學的相關理論會是必要的,而且,我漸漸習慣優先以向量、矩陣來思考,像是OpenSCAD、p5.js等繪圖相關的語言或程式庫,在使用向量與矩陣來實作程式方面,也提供了很大的支援。

有天我突然想到,NumPy既然提供那麼多的向量與矩陣支援,是否也是希望:開發者優先以向量及矩陣來思考?

我試著從這方面來搜尋相關文件進行探索,確實地,NumPy被視為支持陣列程式設計的工具,陣列程式設計指的是,要對個別純量(scalar)的操作,轉換為向量、矩陣、高階陣列的處理。

白話來說,不能只用程式設計者的角度來使用它,要轉換一下看待資料的方式、處理資料時的角度──試著轉換自己為數學家、科學家、物理學家等角色,如果你不懂任何程式語言的情況下,會如何看待與處理資料?

從不使用迴圈開始

身為一個開發者,特別是從命令式(imperative)語言開始學習的人來說,面對重複進行的工作,我們很容易就想到透過迴圈實現。

例如,現在有一組數字,你要怎麼全部都加上10呢?若是採用命令式(Imperative)典範的寫法,基本上會是:

nums = [1, 2, 3]
for i in range(len(nums)):
nums[i] += 10

如果想要一些函數式(Functional)的概念,或許你會使用for comprehension,也就是[n + 10 for n in nums],然而不管是命令式或函數式,很大程度上,都是從程式設計者的角度──從程式流程設計的角度來表現想法,畢竟for comprehension本質上也是一種重複執行的概念,或許你會說,使用遞迴函式封裝起來呢?

例如,Python有個map函式,我們可以撰寫map(lambda n: n + 10, nums1)來得到結果。然而,雖然沒有for了,不過,你還是必須使用lambda,這是程式設計上一級函式的概念,這些對表達想法有幫助嗎?

更進一步地,無論是命令式或是函數式,在處理資料時,都是個別地對nums的每個元素進行處理,而不是將nums當成一個整體來看待。

如果我們使用NumPy的話,若nums=np.array([1,2,3]),nums+10就會是想要的結果,而且,從程式設計者的角度來看,nums是數字組成的陣列,怎麼可以直接加上一個純量呢?然而,換個角度來想,如果不懂程式設計,要對這組數字進行相同的操作,直接加10,在表達上其實就足夠了。

有些介紹NumPy的文件會提到:非到最後關頭,別使用迴圈,而且,當中多半會從效能的角度,說明這麼做的好處(NumPy的陣列操作,在底層是C的實現),只不過,這感覺會與函數式設計的概念重疊。

確實地,如果我們單純將[1,2,3]視為一組數字,那就只是filter、map、reduce層次的概念,然而,如果[1,2,3]代表的是向量呢?例如,表示三維空間中的一個方向?

不單是迴圈的問題

在陣列程式設計中,一組數字不見得就是一組數字,進一步地,還可以用向量的角度來思考,這麼一來,就可以對它進行向量縮放、相加、點積、叉積等運算。

例如,在NumPy當中,v1=np.array([1,2,3]),而且v2=np.array([4,5,6])的話,此時,我們就可以用v1*2、v1+v2、np.dot(v1,v2)、np.cross(v1,v2),來實現相關的處理。

進一步地,一組數字也可以是個矩陣,可以進行矩陣相乘,像是在NumPy中,若m1、m2是代表矩陣的二維陣列,我們就可以使用m1@m2來進行矩陣相乘。

在使用NumPy這類支援陣列程式設計的工具,你必須要思考一下資料的架構方式,從個別的資料中整理出一組一組的資料,之後,讓一組資料能以相同方式來操作、轉換為另一組資料,至於如何整理資料,是在動手寫程式之前就要做的,接著,才是套用NumPy來處理,如此才能發揮出NumPy真正的效用。

如果你拿到的本身就是一組資料,可能會意識到必須這麼做,那麼,我們來看看一個有趣的需求:底下的程式,可以在文字模式中顯示謝爾賓斯基三角形:

for i in range(32):
for j in range(32):
print('■' if i & j == 0 else ' ', end = '')
print()

這看來只是輸出黑或白的問題,要如何轉換,才能將需求看成是資料處理?或具體來說,如何在NumPy中以陣列運算實現需求?

此時,我們可以先用tri=np.arange(32** 2).reshape(n n)建立32x32的二維陣列,接著,將陣列中的每個元素換為'■'或' '字元,然而,在轉換字元時,也要限制不可使用迴圈、for comprehension,或map之類的技術!

NumPy有個frompyfunc可以接受函式,該函式只需要關切陣列中個別元素如何處理,frompyfunc傳回的函式,可以對整個NumPy陣列進行運算,在NumPy中,這稱為向量化,例如:

def bw_symbol(elem, n):
i = elem // n; j = elem % n
return '■' if i & j == 0 else ' '
bw_symbol = np.frompyfunc(bw_symbol, 2, 1)

接著bw_symbol(tri,n)的結果,就是'■'或' '字元組成的二維陣列了,後續的任務是將每一列組合為字串,接著逐行顯示字串,這兩個任務也可以透過NumPy,以整組資料的方式處理,你可以試著實現看看,或者參考〈NumPy的Universal函式〉的程式碼。

以陣列為中心

在使用NumPy時,不以陣列為中心,基本上也能實現需求,開發者在撰寫程式上也比較自由,充其量就是效能差了些,然而,這會失去許多重新思考資料處理方式的機會,也會失去認識更多NumPy的可能性。

以陣列為中心來使用NumPy,是在心智模型加上很強的限制,如此一來,你需要花費更多的心思來分析資料,構思每一組資料的輸入與輸出,卻也能從中進一步地認識資料的本質,以及NumPy中許多技術,像是資料結構或函式等,為何要如此設計的原因!

專欄作者

熱門新聞

Advertisement