天天看點

Python 資料分析——NumPy ndarray對象

作者:昌華量化
Python 資料分析——NumPy ndarray對象

NumPy中使用ndarray對象表示數組,它是整個庫的核心對象,NumPy中所有的函數都是圍繞ndarray對象進行處理的。ndarray的結構并不複雜,但是功能卻十分強大。不但可以用它高效地存儲大量的數值元素,進而提高數組計算的運算速度,還能用它與各種擴充庫進行資料交換。本節的内容可能會有些枯燥,但是為了打下一個良好的基礎,讓我們從深入了解ndarray對象開始學習Python科學計算之旅。

一、建立

首先需要建立數組才能對其進行運算和操作。可以通過給array()函數傳遞Python的序列對象來建立數組,如果傳遞的是多層嵌套的序列,将建立多元數組(下例中的變量c):

a = np.array([1, 2, 3, 4])
b = np.array((5, 6, 7, 8))
c = np.array([[1, 2, 3, 4], [4, 5, 6, 7], [7, 8, 9, 10]])
b c
------------ ------------------
[5, 6, 7, 8] [[ 1, 2, 3, 4],
[ 4, 5, 6, 7],
[ 7, 8, 9, 10]]           

數組的形狀可以通過其shape屬性獲得,它是一個描述數組各個軸的長度的元組(tuple):

a.shape b.shape c.shape
------- ------- -------
(4,) (4,) (3, 4)           

數組a的shape屬性隻有一個元素,是以它是一維數組。而數組c的shape屬性有兩個元素,是以它是二維數組,其中第0軸的長度為3,第1軸的長度為4。還可以通過修改數組的shape屬性,在保持數組元素個數不變的情況下,改變數組每個軸的長度。下面的例子将數組c的shape屬性改為(4,3),注意從(3,4)改為(4,3)并不是對數組進行轉置,而隻是改變每個軸的大小,數組元素在記憶體中的位置并沒有改變:

c.shape = 4, 3
c
array([[ 1, 2, 3],
[ 4, 4, 5],
[ 6, 7, 7],
[ 8, 9, 10]])           

當設定某個軸的元素個數為-1時,将自動計算此軸的長度。由于數組c有12個元素,是以下面的程式将數組c的shape屬性改成了(2,6):

c.shape = 2, -1
c
array([[ 1, 2, 3, 4, 4, 5],
[ 6, 7, 7, 8, 9, 10]])           

使用數組的reshape()方法,可以建立指定形狀的新數組,而原數組的形狀保持不變:

d = a.reshape((2,2)) # 也可以用a.reshape(2,2)
d a
-------- ------------
[[1, 2], [1, 2, 3, 4]
[3, 4]]           

數組a和d其實共享資料存儲空間,是以修改其中任意一個數組的元素都會同時修改另一個數組的内容。注意在下面的例子中,數組d中的2也被改成了100:

a[1] = 100 # 将數組a的第1個元素改為100
a d
-------------------- ------------
[ 1, 100, 3, 4] [[ 1, 100],
[ 3, 4]]           

二、元素類型

數組的元素類型可以通過dtype屬性獲得。在前面的例子中,建立數組所用的序列的元素都是整數,是以所建立的數組的元素類型是整型,并且是32位的長整型。這是因為筆者所使用的Python是32位的,如果使用64位的作業系統和Python,那麼預設整數類型的長度為64位。

c.dtype
dtype('int32')           

可以通過dtype參數在建立數組時指定元素類型,注意float類型是64位的雙精度浮點類型,而complex是128位的雙精度複數類型:

Python 資料分析——NumPy ndarray對象

在上面的例子中,傳遞給dtype參數的都是類型(type)對象,其中float和complex為Python内置的浮點數類型和複數類型,而np.int32是NumPy定義的新的資料類型—— 32位符号整數類型。

NumPy也有自己的浮點數類型:float16、float32、float64和float128。當使用float64作為dtype參數時,其效果和内置的float類型相同。

在需要指定dtype參數時,也可以傳遞一個字元串來表示元素的數值類型。NumPy中的每個數值類型都有幾種字元串表示方式,字元串和類型之間的對應關系都存儲在typeDict字典中。下面的程式獲得與float64類型對應的所有鍵值:

[key for key, value in np.typeDict.items() if value is np.float64]
[12, 'd', 'float64', 'float_', 'float', 'f8', 'double', 'Float64']           

完整的類型清單可以通過下面的語句得到,它将typeDict字典中所有的值轉換為一個集合,進而去除其中的重複項:

Python 資料分析——NumPy ndarray對象

上面顯示的數值類型與數組的dtype屬性是不同的對象。通過dtype對象的type屬性可以獲得與其對應的數值類型:

c.dtype.type
numpy.int32           

通過NumPy的數值類型也可以建立數值對象,下面建立一個16位的符号整數對象,它與Python的整數對象不同的是,它的取值範圍有限,是以計算200*200會溢出,得到一個負數,這一點與C語言的16位整數的結果相同:

a = np.int16(200)
a*a
-25536           

另外值得注意的是,NumPy的數值對象的運算速度比Python的内置類型的運算速度慢很多,如果程式中需要大量地對單個數值運算,應當盡量避免使用NumPy的數值對象。下面比較了Python内置的float類型與NumPy的雙精度浮點數值float64的乘法運算的速度:

v1 = 3.14
v2 = np.float64(v1)
%timeit v1*v1
%timeit v2*v2
10000000 loops, best of 3: 70.1 ns per loop
10000000 loops, best of 3: 178 ns per loop           

使用astype()方法可以對數組的元素類型進行轉換,下面将浮點數數組t1轉換為32位整數數組,将雙精度的複數數組t2轉換成單精度的複數數組:

t1 = np.array([1, 2, 3, 4], dtype=np.float)
t2 = np.array([1, 2, 3, 4], dtype=np.complex)
t3 = t1.astype(np.int32)
t4 = t2.astype(np.complex64)           

三、自動生成數組

前面的例子都是先建立一個Python的序列對象,然後通過array()将其轉換為數組,這樣做顯然效率不高。是以NumPy提供了很多專門用于建立數組的函數。下面的每個函數都有一些關鍵字參數,具體用法請檢視函數說明。

arange()類似于内置函數range(),通過指定開始值、終值和步長來建立表示等差數列的一維數組,注意所得到的結果中不包含終值。例如下面的程式建立開始值為0、終值為1、步長為0.1的等差數組,注意終值1不在數組中:

np.arange(0, 1, 0.1)
array([ 0. , 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9])           

linspace()通過指定開始值、終值和元素個數來建立表示等差數列的一維數組,可以通過endpoint參數指定是否包含終值,預設值為True,即包含終值。下面兩個例子分别示範了endpoint為True和False時的結果,注意endpoint的值會改變數組的等差步長:

Python 資料分析——NumPy ndarray對象
np.linspace(0, 1, 10, endpoint=False) # 步長為1/10
array([ 0. , 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9])           

logspace()和linspace()類似,不過它所建立的數組是等比數列。下面的例子産生從10⁰到10²、有5個元素的等比數列,注意起始值0表示10⁰,而終值2表示10²:

Python 資料分析——NumPy ndarray對象

基數可以通過base參數指定,其預設值為10。下面通過将base參數設定為2,并設定endpoint參數為False,建立一個比例為2^(1/12)的等比數組,此等比數組的比值是音樂中相差半音的兩個音階之間的頻率比值,是以可以用它計算一個八度中所有半音的頻率:

Python 資料分析——NumPy ndarray對象

zeros()、ones()、empty()可以建立指定形狀和類型的數組。其中empty()隻配置設定數組所使用的記憶體,不對數組元素進行初始化操作,是以它的運作速度是最快的。下面的程式建立一個形狀為(2,3)、元素類型為整數的數組,注意其中的元素值沒有被初始化:

np.empty((2,3), np.int)
array([[1078523331, 1065353216, 1073741824],
[1077936128, 1082130432, 1084227584]])           

而zeros()将數組元素初始化為0,ones()将數組元素初始化為1。下面建立一個長度為4、元素類型為整數的一維數組,并且元素全部被初始化為0:

np.zeros(4, np.int)
array([0, 0, 0, 0])           

full()将數組元素初始化為指定的值:

np.full(4, np.pi)
array([ 3.14159265, 3.14159265, 3.14159265, 3.14159265])           

此外,zeros_like()、ones_like()、empty_like()、full_like()等函數建立與參數數組的形狀和類型相同的數組,是以zeros_like(a)和zeros(a.shape, a.dtype)的效果相同。

frombuffer()、fromstring()、fromfile()等函數可以從位元組序列或檔案建立數組。下面以fromstring()為例介紹它們的用法,先建立含8個字元的字元串s:

s = "abcdefgh"           

Python的字元串實際上是一個位元組序列,每個字元占一個位元組。是以如果從字元串s建立一個8位的整數數組,所得到的數組正好就是字元串中每個字元的ASCII編碼:

np.fromstring(s, dtype=np.int8)
array([ 97, 98, 99, 100, 101, 102, 103, 104], dtype=int8)           

如果從字元串s建立16位的整數數組,那麼兩個相鄰的位元組就表示一個整數,把位元組98和位元組97當作一個16位的整數,它的值就是98*256+97 = 25185。可以看出,16位的整數是以低位位元組在前(little-endian)的方式儲存在記憶體中的。

print 98*256+97
np.fromstring(s, dtype=np.int16)
25185
array([25185, 25699, 26213, 26727], dtype=int16)           

如果把整個字元串轉換為一個64位的雙精度浮點數數組,那麼它的值是:

np.fromstring(s, dtype=np.float)
array([ 8.54088322e+194])           

顯然這個結果沒有什麼意義,但是如果我們用C語言的二進制方式寫了一組double類型的數值到某個檔案中,那就可以從此檔案讀取相應的資料,并通過fromstring()将其轉換為float64類型的數組,或者直接使用fromfile()從二進制檔案讀取資料。

fromstring()會對字元串的位元組序列進行複制,而使用frombuffer()建立的數組與原始字元串共享記憶體。由于字元串是隻讀的,是以無法修改所建立的數組的内容:

buf = np.frombuffer(s, dtype=np.int16)
buf[1] = 10
---------------------------------------------------------------------------
ValueError Traceback (most recent call last)
<ipython-input-52-f523db231ae5> in <module>()
1 buf = np.frombuffer(s, dtype=np.int16)
----> 2 buf[1] = 10

ValueError: assignment destination is read-only           

Python中還有一些類型也支援buffer接口,例如bytearray、array.array等。在後面的章節中,我們會介紹如何使用這些對象實作動态數組的功能。

還可以先定義一個從下标計算數值的函數,然後用fromfunction()通過此函數建立數組:

def func(i):
return i % 4 + 1

np.fromfunction(func, (10,))
array([ 1., 2., 3., 4., 1., 2., 3., 4., 1., 2.])           

fromfunction()的第一個參數是計算每個數組元素的函數,第二個參數指定數組的形狀。因為它支援多元數組,是以第二個參數必須是一個序列。上例中第二個參數是長度為1的元組(10,),是以建立了一個有10個元素的一維數組。

下面的例子建立一個表示九九乘法表的二維數組,輸出的數組a中的每個元素a[i, j]都等于func2(i, j):

def func2(i, j):
return (i + 1) * (j + 1)
np.fromfunction(func2, (9,9))
array([[ 1., 2., 3., 4., 5., 6., 7., 8., 9.],
[ 2., 4., 6., 8., 10., 12., 14., 16., 18.],
[ 3., 6., 9., 12., 15., 18., 21., 24., 27.],
[ 4., 8., 12., 16., 20., 24., 28., 32., 36.],
[ 5., 10., 15., 20., 25., 30., 35., 40., 45.],
[ 6., 12., 18., 24., 30., 36., 42., 48., 54.],
[ 7., 14., 21., 28., 35., 42., 49., 56., 63.],
[ 8., 16., 24., 32., 40., 48., 56., 64., 72.],
[ 9., 18., 27., 36., 45., 54., 63., 72., 81.]])           

四、存取元素

可以使用和清單相同的方式對數組的元素進行存取:

a = np.arange(10)
a
array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])           

· a[5]:用整數作為下标可以擷取數組中的某個元素。

· a[3:5]:用切片作為下标擷取數組的一部分,包括a[3]但不包括a[5]。

· a[:5]:切片中省略開始下标,表示從a[0]開始。

· a[:-1]:下标可以使用負數,表示從數組最後往前數。

Python 資料分析——NumPy ndarray對象

· a[1:-1:2]:切片中的第三個參數表示步長,2表示隔一個元素取一個元素。

· a[::-1]:省略切片的開始下标和結束下标,步長為-1,整個數組頭尾颠倒。

· a[5:1:-2]:步長為負數時,開始下标必須大于結束下标。

Python 資料分析——NumPy ndarray對象

下标還可以用來修改元素的值:

a[2:4] = 100, 101
a
array([ 0, 1, 100, 101, 4, 5, 6, 7, 8, 9])           

和清單不同的是,通過切片擷取的新的數組是原始數組的一個視圖。它與原始數組共享同一塊資料存儲空間。下面的程式将b的第2個元素修改為-10,a的第5個元素也同時被修改為-10,因為它們在記憶體中的位址相同。

b = a[3:7] # 通過切片産生一個新的數組b,b和a共享同一塊資料存儲空間
b[2] = -10 # 将b的第2個元素修改為-10
b a
-------------------- --------------------------------------------------
[101, 4, -10, 6] [ 0, 1, 100, 101, 4, -10, 6, 7, 8, 9]           

除了使用切片下标存取元素之外,NumPy還提供了整數清單、整數數組和布爾數組等幾種進階下标存取方法。

當使用整數清單對數組元素進行存取時,将使用清單中的每個元素作為下标。使用清單作為下标得到的數組不和原始數組共享資料:

x = np.arange(10, 1, -1)
x
array([10, 9, 8, 7, 6, 5, 4, 3, 2])           

· x[[3, 3, 1, 8]]:擷取x中的下标為3、3、1、8的4個元素,組成一個新的數組。

· x[[3, 3, -3, 8]]:下标可以是負數,-3表示取倒數第3個元素(從1開始計數)。

a = x[[3, 3, 1, 8]]
b = x[[3, 3, -3, 8]]
a b
------------ ------------
[7, 7, 9, 2] [7, 7, 4, 2]           

下面修改b[2]的值,但是由于它和x不共享記憶體,是以x的值不變:

b[2] = 100
b x
-------------------- ------------------------------------
[ 7, 7, 100, 2] [10, 9, 8, 7, 6, 5, 4, 3, 2]           

整數序列下标也可以用來修改元素的值:

x[[3,5,1]] = -1, -2, -3
x
array([10, -3, 8, -1, 6, -2, 4, 3, 2])           

當使用整數數組作為數組下标時,将得到一個形狀和下标數組相同的新數組,新數組的每個元素都是用下标數組中對應位置的值作為下标從原數組獲得的值。當下标數組是一維數組時,結果和用清單作為下标的結果相同:

x = np.arange(10,1,-1)
x[np.array([3,3,1,8])]
array([7, 7, 9, 2])           

而當下标是多元數組時,得到的也是多元數組:

x[np.array([[3,3,1,8],[3,3,-3,8]])]
array([[7, 7, 9, 2],
[7, 7, 4, 2]])           

可以将上述操作了解為:先将下标數組展平為一維數組,并作為下标獲得一個新的一維數組,然後将其形狀修改為下标數組的形狀:

x[[3,3,1,8,3,3,-3,8]].reshape(2,4) # 改變數組形狀
array([[7, 7, 9, 2],
[7, 7, 4, 2]])           

當使用布爾數組b作為下标存取數組x中的元素時,将獲得數組x中與數組b中True對應的元素。使用布爾數組作為下标獲得的數組不和原始數組共享資料記憶體,注意這種方式隻對應于布爾數組,不能使用布爾清單。

x = np.arange(5,0,-1)
x
array([5, 4, 3, 2, 1])           

布爾數組中下标為0,2的元素為True,是以擷取x中下标為0, 2的元素:

x[np.array([True, False, True, False, False])]
array([5, 3])           

如果是布爾清單,就把True當作1,把False當作0,按照整數序列方式擷取x中的元素:

x[[True, False, True, False, False]]
array([4, 5, 4, 5, 5])           

在NumPy 1.10之後的版本中,布爾清單會被當作布爾數組,是以上面的運作結果會變成array([5, 3])。

布爾數組的長度不夠時,不夠的部分都當作False:

x[np.array([True, False, True, True])]
array([5, 3, 2])           

布爾數組的下标也可以用來修改元素:

x[np.array([True, False, True, True])] = -1, -2, -3
x
array([-1, 4, -2, -3, 1])           

布爾數組一般不是手工産生,而是使用布爾運算的ufunc函數産生,關于ufunc函數請參照下一節的介紹。下面我們舉一個簡單的例子說明布爾數組下标的用法:

x = np.random.randint(0, 10, 6) # 産生一個長度為6,元素值為0到9的随機整數數組
x x > 5
------------------ ------------------------------------------
[8, 1, 5, 6, 2, 7] [ True, False, False, True, False, True]           

表達式x > 5将數組x中的每個元素和5進行大小比較,得到一個布爾數組,True表示x中對應的值大于5。我們可以使用x > 5所得到的布爾數組收集x中所有大于5的數值:

x[x > 5]
array([8, 6, 7])           

五、多元數組

多元數組的存取和一維數組類似,因為多元數組有多個軸,是以它的下标需要用多個值來表示。NumPy采用元組作為數組的下标,元組中的每個元素和數組的每個軸對應。圖1顯示了一個shape為(6, 6)的數組a,圖中用不同顔色和線型标出各個下标所對應的選擇區域。

Python 資料分析——NumPy ndarray對象

圖1 使用數組切片文法通路多元數組中的元素

為什麼使用元組作為下标

Python的下智語法(用[]存取序列中的元素)本身并不支援多元,但是可以使用任何對象作為下标,是以NumPy使用元組作為下标存取數組中的元素,使用元組可以很友善地表示多個軸的下标。雖然在Python程式中經常用圓括号将元組的元素括起來,但其實元組的文法隻需要用逗号隔開元素即可,例如x, y = y, x就是用元組交換變量值的一個例子。是以a[1, 2]和a[(1, 2)]完全相同,都是使用元組(1,2)作為數組a的下标。

讀者也許會對如何建立圖中的二維數組感到好奇。它實際上是一個加法表,由縱向量(0, 10, 20, 30, 40, 50)和橫向量(0, 1, 2, 3, 4, 5)的元素相加而得。可以用下面的語句建立它。

a = np.arange(0, 60, 10).reshape(-1, 1) + np.arange(0, 6)
a
array([[ 0, 1, 2, 3, 4, 5],
[10, 11, 12, 13, 14, 15],
[20, 21, 22, 23, 24, 25],
[30, 31, 32, 33, 34, 35],
[40, 41, 42, 43, 44, 45],
[50, 51, 52, 53, 54, 55]])           

圖1中的下标都是有兩個元素的元組,其中的第0個元素與數組的第0軸(縱軸)對應,而第1個元素與數組的第1軸(橫軸)對應。下面是圖中各種多元數組切片的運算結果:

Python 資料分析——NumPy ndarray對象

如果下标元組中隻包含整數和切片,那麼得到的數組和原始數組共享資料,它是原數組的視圖。下面的例子中,數組b是a的視圖,它們共享資料,是以修改b[0]時,數組a中對應的元素也被修改:

b = a[0, 3:5]
b[0] = -b[0]
a[0, 3:5]
array([-3, 4])           

因為數組的下标是一個元組,是以我們可以将下标元組儲存起來,用同一個元組存取多個數組。在下面的例子中,a[idx]和a[::2,2:]相同,a[idx][idx]和a[::2,2:][::2,2:]相同。

idx = slice(None, None, 2), slice(2,None)
a[idx] a[idx][idx]
-----------------------------
[[ 2, -3, 4, 5], [[ 4, 5],
[22, 23, 24, 25], [44, 45]]
[42, 43, 44, 45]]           

切片(slice)對象

根據Python的文法,在[]中可以使用以冒号隔開的兩個或三個整數表示切片,但是單獨生成切片對象時需要使用slice()來建立。它有三個參數,分别為開始值、結束值和間隔步長,當這些值需要省略時可以使用None。例如,a[slice(None,None,None),2]和a[:, 2]相同。

用Python的内置函數slice()建立下标比較麻煩,是以NumPy提供了一個s_對象來幫助我們建立數組下标,請注意s_實際上是IndexExpression類的一個對象:

np.s_[::2, 2:]
(slice(None, None, 2), slice(2, None, None))           

s_為什麼不是函數

根據Python的文法,隻有在中括号[]中才能使用以冒号隔開的切片文法,如果s_是函數,那麼這些切片必須使用slice()建立。類似的對象還有mgrid和ogrid等,後面我們會學習它們的用法。Python的下智語法實際上會調用__getitem__()方法,是以我們可以很容易自己實作s_對象的功能:

class S(object):
def __getitem__(self, index):
return index           

在多元數組的下标元組中,也可以使用整數元組或清單、整數數組和布爾數組,如圖2所示。當下标中使用這些對象時,所獲得的資料是原始資料的副本,是以修改結果數組不會改變原始數組。

Python 資料分析——NumPy ndarray對象

圖2 使用整數序列和布爾數組通路多元數組中的元素

在a[(0,1,2,3),(1,2,3,4)]中,下标仍然是一個有兩個元素的元組,元組中的每個元素都是一個整數元組,分别對應數組的第0軸和第1軸。從兩個序列的對應位置取出兩個整數組成下标,于是得到的結果是:a[0,1]、a[1,2]、a[2,3]、a[3,4]。

a[(0,1,2,3),(1,2,3,4)]
array([ 1, 12, 23, 34])           

在a[3:, [0,2,5]]中,第0軸的下标是一個切片對象,它選取第3行之後的所有行;第1軸的下标是整數清單,它選取第0、第2和第5列。

a[3:, [0,2,5]]
array([[30, 32, 35],
[40, 42, 45],
[50, 52, 55]])           

在a[mask, 2]中,第0軸的下标是一個布爾數組,它選取第0、第2和第5行;第1軸的下标是一個整數,它選取第2列。

mask = np.array([1,0,1,0,0,1], dtype=np.bool)
a[mask, 2]
array([ 2, 22, 52])           

注意,如果mask不是布爾數組而是整數數組、清單或元組,就按照以整數數組作為下标的方式進行運算:

mask1 = np.array([1,0,1,0,0,1])
mask2 = [True,False,True,False,False,True]
a[mask1, 2] a[mask2, 2]
------------------------ ------------------------
[12, 2, 12, 2, 2, 12] [12, 2, 12, 2, 2, 12]           

當下标的長度小于數組的維數時,剩餘的各軸所對應的下标是“:”,即選取它們的所有資料:

a[[1,2],:] a[[1,2]]
-------------------------- --------------------------
[[10, 11, 12, 13, 14, 15], [[10, 11, 12, 13, 14, 15],
[20, 21, 22, 23, 24, 25]] [20, 21, 22, 23, 24, 25]]           

當所有軸都用形狀相同的整數數組作為下标時,得到的數組和下标數組的形狀相同:

x = np.array([[0,1],[2,3]])
y = np.array([[-1,-2],[-3,-4]])
a[x,y]
array([[ 5, 14],
[23, 32]])           

效果和下面的程式相同:

a[(0,1,2,3),(-1,-2,-3,-4)].reshape(2,2)
array([[ 5, 14],
[23, 32]])           

當沒有指定第1軸的下标時,使用“:”作為下标,是以得到了一個三維數組:

a[x]
array([[[ 0, 1, 2, -3, 4, 5],
[10, 11, 12, 13, 14, 15]],

[[20, 21, 22, 23, 24, 25],
[30, 31, 32, 33, 34, 35]]])           

可以使用這種以整數數組作為下标的方式快速替換數組中的每個元素,例如有一個表示索引圖像的數組image,以及一個調色闆數組palette,則palette[image]可以得到通過調色闆着色之後的彩色圖像:

palette = np.array( [ [0,0,0],
[255,0,0],
[0,255,0],
[0,0,255],
[255,255,255] ] )
image = np.array( [ [ 0, 1, 2, 0 ],
[ 0, 3, 4, 0 ] ] )
palette[image]
array([[[ 0, 0, 0],
[255, 0, 0],
[ 0, 255, 0],
[ 0, 0, 0]],

[[ 0, 0, 0],
[ 0, 0, 255],
[255, 255, 255],
[ 0, 0, 0]]])           

六、結構數組

在C語言中我們可以通過struct關鍵字定義結構類型,結構中的字段占據連續的記憶體空間。類型相同的兩個結構所占用的記憶體大小相同,是以可以很容易定義結構數組。和C語言一樣,在NumPy中也很容易對這種結構數組進行操作。隻要NumPy中的結構定義和C語言中的結構定義相同,就可以很友善地讀取C語言的結構數組的二進制資料,将其轉換為NumPy的結構數組。

假設我們需要定義一個結構數組,它的每個元素都有name、age和weight字段。在NumPy中可以如下定義:

persontype = np.dtype({ ❶
'names':['name', 'age', 'weight'],
'formats':['S30','i', 'f']}, align=True)
a = np.array([("Zhang", 32, 75.5), ("Wang", 24, 65.2)], ❷
dtype=persontype)           

❶我們先建立一個dtype對象persontype,它的參數是一個描述結構類型的各個字段的字典。字典有兩個鍵:'names'和'formats'。每個鍵對應的值都是一個清單。'names'定義結構中每個字段的名稱,而'formats'則定義每個字段的類型。這裡我們使用類型字元串定義字段類型:

· 'S30':長度為30個位元組的字元串類型,由于結構中的每個元素的大小必須固定,是以需要指定字元串的長度。

· 'i':32位的整數類型,相當于np.int32。

· 'f':32位的單精度浮點數類型,相當于np.float32。

❷然後調用array()以建立數組,通過dtype參數指定所建立的數組的元素類型為persontype。下面檢視數組a的元素類型:

a.dtype
dtype({'names':['name','age','weight'], 'formats':['S30','<i4','<f4'],
'offsets':[0,32,36], 'itemsize':40}, align=True)           

還可以用包含多個元組的清單來描述結構的類型:

dtype([('name', '|S30'), ('age', '<i4'), ('weight', '<f4')])           

其中形如“(字段名, 類型描述)”的元組描述了結構中的每個字段。類型字元串前面的'|'、'<'、'>'等字元表示字段值的位元組順序:

· |:忽視位元組順序。

· lt;:低位位元組在前,即小端模式(little endian)。

· >:高位位元組在前,即大端模式(big endian)。

結構數組的存取方式和一般數組相同,通過下标能夠取得其中的元素,注意元素的值看上去像是元組,實際上是結構:

print a[0]
a[0].dtype
('Zhang', 32, 75.5)
dtype({'names':['name','age','weight'], 'formats':['S30','<i4','<f4'],
'offsets':[0,32,36], 'itemsize':40}, align=True)           

我們可以使用字段名作為下标擷取對應的字段值:

a[0]["name"]
'Zhang'           

a[0]是一個結構元素,它和數組a共享記憶體資料,是以可以通過修改它的字段來改變原始數組中對應元素的字段:

c = a[1]
c["name"] = "Li"
a[1]["name"]
'Li'           

我們不但可以獲得結構元素的某個字段,而且可以直接獲得結構數組的字段,傳回的是原始數組的視圖,是以可以通過修改b[0]來改變a[0]["age"]:

b=a["age"]
b[0] = 40
print a[0]["age"]
40           

通過a.tostring()或a.tofile()方法,可以将數組a以二進制的方式轉換成字元串或寫入檔案:

a.tofile("test.bin")           

利用下面的C語言程式可以将test.bin檔案中的資料讀取出來。%%file為IPython的魔法指令,它将該單元格中的文本儲存成檔案read_struct_array.c:

%%file read_struct_array.c
#include <stdio.h>

struct person
{
char name[30];
int age;
float weight;
};

struct person p[3];

void main ()
{
FILE *fp;
int i;
fp=fopen("test.bin","rb");
fread(p, sizeof(struct person), 2, fp);
fclose(fp);
for(i=0;i<2;i++)
{
printf("%s %d %f\n", p[i].name, p[i].age, p[i].weight);
}
}           

在IPython中可以通過!執行系統指令,下面調用gcc編譯前面的C語言程式并執行:

!gcc read_struct_array.c -o read_struct_array.exe
!read_struct_array.exe
Zhang 40 75.500000
Li 24 65.199997           

記憶體對齊

為了記憶體尋址友善,C語言的結構類型會自動添加一些填充用的位元組,這叫做記憶體對齊。例如上面C語言中定義的結構的name字段雖然是30個位元組長,但是由于記憶體對齊問題,在name和age中間會填補兩個位元組。是以,如果數組中所配置的記憶體大小不符合C語言的對齊規範,将會出現資料錯位。為了解決這個問題,在建立dtype對象時,可以傳遞參數align=True,這樣結構數組的記憶體對齊就和C語言的結構類型一緻了。在前面的例子中,由于建立persontype時指定align參數為True,是以它占用40個位元組。

結構類型中可以包括其他的結構類型,下面的語句建立一個有一個字段f1的結構,f1的值是另一個結構,它有字段f2,類型為16位整數:

np.dtype([('f1', [('f2', np.int16)])])
dtype([('f1', [('f2', '<i2')])])           

當某個字段類型為數組時,用元組的第三個元素表示其形狀。在下面的結構體中,f1字段是一個形狀為(2, 3)的雙精度浮點數組:

np.dtype([('f0', 'i4'), ('f1', 'f8', (2, 3))])
dtype([('f0', '<i4'), ('f1', '<f8', (2, 3))])           

用下面的字典參數也可以定義結構類型,字典的鍵為結構的字段名,值為字段的類型描述。但是由于字典的鍵是沒有順序的,是以字段的順序需要在類型描述中給出。類型描述是一個元組,它的第二個值給出字段的以位元組為機關的偏移量,例如下例中的age字段的偏移量為25個位元組:

np.dtype({'surname':('S25',0),'age':(np.uint8,25)})
dtype([('surname', 'S25'), ('age', 'u1')])           

七、記憶體結構

下面讓我們看看數組對象是如何在記憶體中存儲的。如圖3所示,數組的描述資訊儲存在一個資料結構中,這個結構引用兩個對象:用于儲存資料的存儲區域和用于描述元素類型的dtype對象。

Python 資料分析——NumPy ndarray對象

圖3 ndarray數組對象在記憶體中的存儲方式

資料存儲區域儲存着數組中所有元素的二進制資料,dtype對象則知道如何将元素的二進制資料轉換為可用的值。數組的維數和形狀等資訊都儲存在ndarray數組對象的資料結構中。圖3中顯示的是下面的數組a的記憶體結構:

a = np.array([[0,1,2],[3,4,5],[6,7,8]], dtype=np.float32)           

數組對象使用strides屬性儲存每個軸上相鄰兩個元素的位址差,即當某個軸的下标增加1時,資料存儲區中的指針所增加的位元組數。例如圖2-3中的strides為(12,4),即第0軸的下标增加1時,資料的位址增加12個位元組。也就是a[1,0]的位址比a[0,0]的位址大12,正好是3個單精度浮點數的總位元組數。第1軸的下标增加1時,資料的位址增加4個位元組,正好是一個單精度浮點數的位元組數。

如果strides屬性中的數值正好和對應軸所占據的位元組數相同,那麼資料在記憶體中是連續存儲的。通過切片下标得到的新數組是原始數組的視圖,即它和原始數組共享資料存儲區域,但是新數組的strides屬性會發生變化:

b = a[::2, ::2]
b b.strides
------------ ---------
[[ 0., 2.], (24, 8)
[ 6., 8.]]           

由于數組b和數組a共享資料存儲區,而數組b中的第0軸和第1軸都是從a中隔一個元素取一個,是以數組b的strides變成了(24, 8),正好都是數組a的兩倍。對照前面的圖2-3很容易看出資料0和2的位址相差8個位元組,而資料0和6的位址相差24個位元組。

元素在資料存儲區中的排列格式有兩種:C語言格式和Fortran語言格式。在C語言中,多元數組的第0軸是最上位的,即第0軸的下标增加1時,元素的位址增加的位元組數最多;而Fortran語言中的多元數組的第0軸是最下位的,即第0軸的下标增加1時,位址隻增加一個元素的位元組數。在NumPy中預設以C語言格式存儲資料,如果希望改為Fortran格式,隻需要在建立數組時,設定order參數為"F":

c = np.array([[0,1,2],[3,4,5],[6,7,8]], dtype=np.float32, order="F")
c.strides
(4, 12)           

了解了數組的記憶體結構,就可以解釋使用下标取得資料時的複制和引用問題:

· 當下标使用整數和切片時,所取得的資料在資料存儲區域中是等間隔分布的。因為隻需要修改圖2-3所示的資料結構中的dim count、dimensions、stride等屬性以及指向資料存儲區域的指針data,就能實作整數和切片下标,是以新數組和原始數組能夠共享資料存儲區域。

· 當使用整數序列、整數數組和布爾數組時,不能保證所取得的資料在資料存儲區域中是等間隔的,是以無法和原始數組共享資料,隻能對資料進行複制。

數組的flags屬性描述了資料存儲區域的一些屬性,直接檢視flags屬性将輸出各個标志的值,也可以單獨獲得其中的某個标志值:

print a.flags
print "c_contiguous:", a.flags.c_contiguous
C_CONTIGUOUS : True
F_CONTIGUOUS : False
OWNDATA : True
WRITEABLE : True
ALIGNED : True
UPDATEIFCOPY : False
c_contiguous: True           

下面是幾個比較重要的标志:

· C_CONTIGUOUS:資料存儲區域是否是C語言格式的連續區域。

· F_CONTIGUOUS:資料存儲區域是否是Fortran語言格式的連續區域。

· OWNDATA:數組是否擁有此資料存儲區域,當一個數組是其他數組的視圖時,它不擁有資料存儲區域。

由于數組a是通過array()直接建立的,是以它的資料存儲區域是C語言格式的連續區域,并且它擁有資料存儲區域。下面我們看看數組a的轉置标志,數組的轉置可以通過其T屬性獲得,轉置數組将其資料存儲區域看作Fortran語言格式的連續區域,并且它不擁有資料存儲區域。

a.T.flags
C_CONTIGUOUS : False
F_CONTIGUOUS : True
OWNDATA : False
WRITEABLE : True
ALIGNED : True
UPDATEIFCOPY : False           

下面檢視數組b的标志,它不擁有資料存儲區域,其資料也不是連續存儲的。通過視圖數組的base屬性可以獲得儲存資料的原始數組:

b.flags
C_CONTIGUOUS : False
F_CONTIGUOUS : False
OWNDATA : False
WRITEABLE : True
ALIGNED : True
UPDATEIFCOPY : False
id(b.base) id(a)
---------- ---------
119627272 119627272           

我們還可以通過view()方法從同一塊資料區建立不同的dtype的數組對象,也就是使用不同的數值類型檢視同一段記憶體中的二進制資料:

Python 資料分析——NumPy ndarray對象

由于數組a的元素類型是單精度浮點數,占用4個位元組,通過a.view(np.uint32),我們建立了一個新的數組,它和數組a使用同一段資料記憶體,但是它将每4個位元組的資料當作無符号32位整數處理。而a.view(np.uint8)将每個位元組都當作一個單位元組的無符号整數,是以得到一個形狀為(3, 8)的數組。通過view()方法獲得的新數組與原數組共享記憶體,當a[0, 0]被修改時,b[0, 0]和c[0, :4]都會改變:

a[0, 0] = 3.14
b[0, 0] c[0, :4]
---------- --------------------
1078523331 [195, 245, 72, 64]