Numerical Python (通常稱為 NumPy) 是一個廣為應用的 Python 擴展庫,用於快速處理任意維數的固定類型數組。由於底層代碼是充分優化的 C 語言代碼,因而對數組的主要操作在 NumPy 調用中執行時,速度不再受到 Python 解釋器的限制。因為 NumPy 已經取得了這樣的成功,所以 NumPy 的開發者將用一個叫做 Numarray 的新模塊來取代 NumPy,新模塊基本上 (但並不是完全) 與 NumPy 兼容。在本文中,David 介紹了 NumPy 的一般功能,以及 Numarray 將要帶來的一些特殊改進。 要了解 Numerical Python 軟件包的第一件事情是,Numerical Python 不會讓您去做標准 Python 不能完成的任何工作。它只是讓您 以快得多的速度 去完成標准 Python 能夠完成的相同任務。實際上不僅僅如此;許多數組操作用 Numeric 或者 Numarray 來表達比起用標准 Python 數據類型和語法來表達要優雅得多。不過,驚人的速度才是吸引用戶使用 Numerical Python 的主要原因。 其實,Numerical Python 只是實現了一個新的數據類型:數組。與可以包含不同類型元素的列表、元組和詞典不同的是,Numarray 數組只能包含同一類型的數據。Numarray 數組的另一個優點是,它可以是多維的 -- 但是數組的維度與列表的簡單嵌套稍有不同。Numerical Python 借鑒了程序員的實踐經驗(尤其是那些有科學計算背景的程序員,他們抽象出了 APL、FORTRAN、MATLAB 和 S 等語言中數組的最佳功能),創建了可以靈活改變形狀和維度的數組。我們很快會回來繼續這一話題。 在 Numerical Python 中對數組的操作是 按元素 進行的。雖然二維數組與線性代數中的矩陣類似,但是對它們的操作 (比如乘) 與線性代數中的操作 (比如矩陣乘) 是完全不同的。 讓我們來看一個關於上述問題的的具體例子。在純 Python 中,您可以這樣創建一個“二維列表”:
清單 1. Python 的嵌套數組 >>> pyarr = [[1,2,3], ... [4,5,6], ... [7,8,9]] >>> print pyarr [[1, 2, 3], [4, 5, 6], [7, 8, 9]] >>> pyarr[1][1] = 0 >>> print pyarr [[1, 2, 3], [4, 0, 6], [7, 8, 9]] 很好,但是您對這種結構所能做的只是通過單獨的 (或者多維的) 索引來設置和檢索元素。與此相比,Numarray 數組要更靈活:
清單 2. Numerical Python 數組 >>> from numarray import * >>> numarr = array(pyarr) >>> print numarr [[1 2 3] [4 0 6] [7 8 9]] 改變並不大,但是使用 Numarray 進行的操作如何呢? 下面是一個例子:
清單 3. 元素操作 >>> numarr2 = numarr * 2 >>> print numarr2 [[ 2 4 6] [ 8 0 12] [14 16 18]] >>> print numarr2 + numarr [[ 3 6 9] [12 0 18] [21 24 27]] 改變數組的形狀:
清單 4. 改變形狀 >>> numarr2.shape = (9,) >>> print numarr2 [ 2 4 6 8 0 12 14 16 18] Numeric 與 Numarray 之間的區別 總體來看,新的 Numarray 軟件包與早期的 Numeric 是 API 兼容的。不過,開發者基於用戶經驗進行了一些與 Numric 並不兼容的改進。開發者沒有破壞任何依賴於 Numeric 的應用程序,而是開創了一個叫做 Numarray 的新項目。在完成本文時,Numarray 還缺少 Numeric 的一些功能,但是已計劃實現這些功能。 Numarray 所做的一些改進: 以分層的類結構來組織元素類型,以支持 isinstance() 檢驗。Numeric 在指定數據類型時只使用字符類型編碼 (但是 Numarray 中的初始化軟件仍然接受老的字符編碼)。 改變了類型強制規則,以保持數組(更為常見)中的類型 ,而不是轉換為 Python 標量的類型。 出現了附加的數組屬性 (不再只有 getter 和 setter)。 實現了更靈活的異常處理。 新用戶不必擔心這些變化,就這一點來說,最好一開始就使用 Numarray 而不是 Numeric。
計時的例子 讓我們來感受一下在 Numerical Python 中的操作相對於標准 Python 的速度優勢。作為一個“演示任務”,我們將創建一個數字序列,然後使它們加倍。首先是標准 Python 方法的一些變體:
清單 5. 對純 Python 操作的計時 def timer(fun, n, comment=""): from time import clock start = clock() print comment, len(fun(n)), "elements", print "in %.2f seconds" % (clock()-start) def double1(n): return map(lambda n: 2*n, xrange(n)) timer(double1, 5000000, "Running map() on xrange iterator:") def double2(n): return [2*n for n in xrange(n)] timer(double2, 5000000, "Running listcomp on xrange iter: ") def double3(n): double = [] for n in xrange(n): double.append(2*n) return double timer(double3, 5000000, "Building new list from iterator: ") 我們可以看出 map() 方法、list comprehension 和傳統循環方法之間的速度差別。那麼,需要同類元素類型的標准 array 模塊呢?它可能會更快一些:
清單 6. 對標准 array 模塊的計時 import array def double4(n): return [2*n for n in array.array('i',range(n))] timer(double4, 5000000, "Running listcomp on array.array: ") 最後我們來看 Numarray 的速度如何。作為額外對照,我們來看如果必須要將數組還原為一個標准的列表時,它是否同樣具有優勢:
清單 7. 對 Numarray 操作的計時 from numarray import * def double5(n): return 2*arange(n) timer(double5, 5000000, "Numarray scalar multiplication: ") def double6(n): return (2*arange(n)).tolist() timer(double6, 5000000, "Numarray mult, returning list: ") 現在運行它:
清單 8. 比較結果 $ python2.3 timing.py Running map() on xrange iterator: 5000000 elements in 13.61 seconds Running listcomp on xrange iter: 5000000 elements in 16.46 seconds Building new list from iterator: 5000000 elements in 20.13 seconds Running listcomp on array.array: 5000000 elements in 25.58 seconds Numarray scalar multiplication: 5000000 elements in 0.61 seconds Numarray mult, returning list: 5000000 elements in 3.70 seconds 處理列表的不同技術之間的速度差異不大,也許還是值得注意,因為這是嘗試標准的 array 模塊時的方法問題。但是 Numarray 一般用不到 1/20 的時間內就可以完成操作。將數組還原為標准列表損失了很大的速度優勢。 不應通過這樣一個簡單的比較就得出結論,但是這種加速可能是典型的。對大規模科學計算來說,將計算的時間由幾個月下降到幾天或者從幾天下降到幾個小時,是非常有價值的。
系統建模 Numerical Python 的典型用例是科學建模,或者可能是相關領域,比如圖形處理和旋轉,或者信號處理。我將通過一個比較實際的問題來說明 Numarray 的許多功能。假設您有一個參量可變的三維物理空間。抽象地說,任何參數化空間,不論有多少維,Numarray 都適用。實際上很容易想像,比如一個房間,它的各個點的溫度是不同的。我在 New England 的家已經到了冬天,因而這個問題似乎更有現實意義。 為簡單起見,下面我給出的例子中使用的是較小的數組(雖然這可能是顯然的,但是還是有必要明確地指出來)。不過,即使是處理有上百萬個元素而不僅僅是幾十個元素的數組,Numarray 也還是很快;前者可能在真正的科學模型中更為常見。 首先,我們來創建一個“房間”。有很多方法可以完成這項任務,但是最常用的還是使用可調用的 array() 方法。使用這個方法,我們可以生成具有多種初始化參數 (包括來自任何 Python 序列的初始數據) 的 Numerical 數組。不過對於我們的房間來說,用 zeros() 函數就可以生成一個溫度均勻的寒冷房間:
清單 9. 初始化房間的溫度 >>> from numarray import * >>> room = zeros((4,3,5),Float) >>> print room [[[ 0. 0. 0. 0. 0.] [ 0. 0. 0. 0. 0.] [ 0. 0. 0. 0. 0.]] [[ 0. 0. 0. 0. 0.] [ 0. 0. 0. 0. 0.] [ 0. 0. 0. 0. 0.]] [[ 0. 0. 0. 0. 0.] [ 0. 0. 0. 0. 0.] [ 0. 0. 0. 0. 0.]] [[ 0. 0. 0. 0. 0.] [ 0. 0. 0. 0. 0.] [ 0. 0. 0. 0. 0.]]] 自上而下每一個二維的“矩陣”代表三維房間的一個水平層面。 首先,我們將整個房間的溫度提高到比較舒適的 70 華氏度 (大約是 20 攝氏度):
清單 10. 打開加熱器 >>> room += 70 >>> print room [[[ 70. 70. 70. 70. 70.] [ 70. 70. 70. 70. 70.] [ 70. 70. 70. 70. 70.]] [[ 70. 70. 70. 70. 70.] [ 70. 70. 70. 70. 70.] [ 70. 70. 70. 70. 70.]] [[ 70. 70. 70. 70. 70.] [ 70. 70. 70. 70. 70.] [ 70. 70. 70. 70