一、前言
我們知道,python是一種動態語言,可以將任何類型的數據賦給任何變量,譬如:
# Python代碼 x = 4 x = "four"
這里已經將 x
變量的內容由整型轉變成了字符串,而同樣的操作在 C 語言中將會導致(取決於編譯器設置)編譯錯誤或其他未知的后果。
這種靈活性是使 Python 和其他動態類型的語言更易用的原因之一。理解這一特性如何工作是學習用 Python 有效且高效地分析數據的重要因素。但是這種類型靈活性也指出了一個事實:
Python 變量不僅是它們的值,還包括了關於值的類型的一些額外信息。
二、整型
標准的 Python 實現是用 C 語言編寫的。這意味着每一個 Python 對象都是一個偽 C 語言結構體,該結構體不僅包含其值,還有其他信息。例如,當我們在 Python 中定義一個整型,例如 x = 10000
時,x
並不是一個“原生”整型,而是一個指針,指向一個 C 語言的復合結構體,結構體里包含了一些值。查看 Python 3.4 的源代碼,可以發現整型(長整型)的定義,如下所示(C 語言的宏經過擴展之后):
struct _longobject { long ob_refcnt; PyTypeObject *ob_type; size_t ob_size; long ob_digit[1]; };
Python 3.4 中的一個整型實際上包括 4 個部分。
ob_refcnt
是一個引用計數,它幫助 Python 默默地處理內存的分配和回收。ob_type
將變量的類型編碼。ob_size
指定接下來的數據成員的大小。ob_digit
包含我們希望 Python 變量表示的實際整型值。
這意味着與 C 語言這樣的編譯語言中的整型相比,在 Python 中存儲一個整型會有一些開銷,正如下圖所示:
這里 PyObject_HEAD
是結構體中包含引用計數、類型編碼和其他之前提到的內容的部分。
兩者的差異在於,C 語言整型本質上是對應某個內存位置的標簽,里面存儲的字節會編碼成整型。而 Python 的整型其實是一個指針,指向包含這個 Python 對象所有信息的某個內存位置,其中包括可以轉換成整型的字節。由於 Python 的整型結構體里面還包含了大量額外的信息,所以 Python 可以自由、動態地編碼。但是,Python 類型中的這些額外信息也會成為負擔,在多個對象組合的結構體中尤其明顯。
三、列表
設想如果使用一個包含很多 Python 對象的 Python 數據結構,會發生什么? Python 中的標准可變多元素容器是列表。可以用如下方式創建一個整型值列表:
In[1]: L = list(range(10)) L Out[1]: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] In[2]: type(L[0]) Out[2]: int
或者創建一個字符串列表:
In[3]: L2 = [str(c) for c in L] L2 Out[3]: ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'] In[4]: type(L2[0]) Out[4]: str
因為 Python 的動態類型特性,甚至可以創建一個異構的列表:
In[5]: L3 = [True, "2", 3.0, 4] [type(item) for item in L3] Out[5]: [bool, str, float, int]
但是想擁有這種靈活性也是要付出一定代價的:為了獲得這些靈活的類型,列表中的每一項必須包含各自的類型信息、引用計數和其他信息;也就是說,每一項都是一個完整的 Python 對象。來看一個特殊的例子,如果列表中的所有變量都是同一類型的,那么很多信息都會顯得多余——將數據存儲在固定類型的數組中應該會更高效。動態類型的列表和固定類型的(NumPy 式)數組間的區別如下圖所示。
在實現層面,數組基本上包含一個指向連續數據塊的指針。另一方面,Python 列表包含一個指向指針塊的指針,這其中的每一個指針對應一個完整的 Python 對象(如前面看到的 Python 整型)。另外,列表的優勢是靈活,因為每個列表元素是一個包含數據和類型信息的完整結構體,而且列表可以用任意類型的數據填充。固定類型的 NumPy 式數組缺乏這種靈活性,但是能更有效地存儲和操作數據。
四、固定類型數組
1. 使用array模塊創建數組
Python 提供了幾種將數據存儲在有效的、固定類型的數據緩存中的選項。內置的數組(array
)模塊(在 Python 3.3 之后可用)可以用於創建統一類型的密集數組:
In[6]: import array L = list(range(10)) A = array.array('i', L) A Out[6]: array('i', [0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
這里的 'i'
是一個數據類型碼,表示數據為整型。
更實用的是 NumPy 包中的 ndarray
對象。Python 的數組對象提供了數組型數據的有效存儲,而 NumPy 為該數據加上了高效的操作。稍后將介紹這些操作,這里先展示幾種創建 NumPy 數組的方法。
從用 np
別名導入 NumPy 的標准做法開始:
In[7]: import numpy as np
2. 從python列表創建NumPy 數組
首先,可以用 np.array
從 Python 列表創建數組:
In[8]: # 整型數組: np.array([1, 4, 2, 5, 3]) Out[8]: array([1, 4, 2, 5, 3])
請記住,不同於 Python 列表,NumPy 要求數組必須包含同一類型的數據。如果類型不匹配,NumPy 將會向上轉換(如果可行)。這里整型被轉換為浮點型:
In[9]: np.array([3.14, 4, 2, 3])
Out[9]: array([ 3.14, 4. , 2. , 3. ])
如果希望明確設置數組的數據類型,可以用 dtype
關鍵字:
In[10]: np.array([1, 2, 3, 4], dtype='float32') Out[10]: array([ 1., 2., 3., 4.], dtype=float32)
最后,不同於 Python 列表,NumPy 數組可以被指定為多維的。以下是用列表的列表初始化多維數組的一種方法:
In[11]: # 嵌套列表構成的多維數組 np.array([range(i, i + 3) for i in [2, 4, 6]]) Out[11]: array([[2, 3, 4], [4, 5, 6], [6, 7, 8]])
內層的列表被當作二維數組的行。
3. 從頭創建NumPy 數組
面對大型數組的時候,用 NumPy 內置的方法從頭創建數組是一種更高效的方法。以下是幾個示例:
In[12]: # 創建一個長度為10的數組,數組的值都是0 np.zeros(10, dtype=int) Out[12]: array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0]) In[13]: # 創建一個3×5的浮點型數組,數組的值都是1 np.ones((3, 5), dtype=float) Out[13]: array([[ 1., 1., 1., 1., 1.], [ 1., 1., 1., 1., 1.], [ 1., 1., 1., 1., 1.]]) In[14]: # 創建一個3×5的浮點型數組,數組的值都是3.14 np.full((3, 5), 3.14) Out[14]: array([[ 3.14, 3.14, 3.14, 3.14, 3.14], [ 3.14, 3.14, 3.14, 3.14, 3.14], [ 3.14, 3.14, 3.14, 3.14, 3.14]]) In[15]: # 創建一個3×5的浮點型數組,數組的值是一個線性序列 # 從0開始,到20結束,步長為2 # (它和內置的range()函數類似) np.arange(0, 20, 2) Out[15]: array([ 0, 2, 4, 6, 8, 10, 12, 14, 16, 18]) In[16]: # 創建一個5個元素的數組,這5個數均勻地分配到0~1 np.linspace(0, 1, 5) Out[16]: array([ 0. , 0.25, 0.5 , 0.75, 1. ]) In[17]: # 創建一個3×3的、在0~1均勻分布的隨機數組成的數組 np.random.random((3, 3)) Out[17]: array([[ 0.99844933, 0.52183819, 0.22421193], [ 0.08007488, 0.45429293, 0.20941444], [ 0.14360941, 0.96910973, 0.946117 ]]) In[18]: # 創建一個3×3的、均值為0、方差為1的 # 正態分布的隨機數數組 np.random.normal(0, 1, (3, 3)) Out[18]: array([[ 1.51772646, 0.39614948, -0.10634696], [ 0.25671348, 0.00732722, 0.37783601], [ 0.68446945, 0.15926039, -0.70744073]]) In[19]: # 創建一個3×3的、[0, 10)區間的隨機整型數組 np.random.randint(0, 10, (3, 3)) Out[19]: array([[2, 3, 4], [5, 7, 8], [0, 5, 0]]) In[20]: # 創建一個3×3的單位矩陣 np.eye(3) Out[20]: array([[ 1., 0., 0.], [ 0., 1., 0.], [ 0., 0., 1.]]) In[21]: # 創建一個由3個整型數組成的未初始化的數組 # 數組的值是內存空間中的任意值 np.empty(3) Out[21]: array([ 1., 1., 1.])
五、NumPy標准數據類型
NumPy 數組包含同一類型的值,因此詳細了解這些數據類型及其限制是非常重要的。因為 NumPy 是在 C 語言的基礎上開發的,所以 C、Fortran 和其他類似語言的用戶會比較熟悉這些數據類型。
數據類型 |
描述 |
bool_ |
布爾值(真、True 或假、False),用一個字節存儲 |
int_ |
默認整型(類似於 C 語言中的 long,通常情況下是 int64 或 int32) |
intc |
同 C 語言的 int 相同(通常是 int32 或 int64) |
intp |
用作索引的整型(和 C 語言的 ssize_t 相同,通常情況下是 int32 或 int64) |
int8 |
字節(byte,范圍從–128 到 127) |
int16 |
整型(范圍從–32768 到 32767) |
int32 |
整型(范圍從–2147483648 到 2147483647) |
int64 |
整型(范圍從–9223372036854775808 到 9223372036854775807) |
uint8 |
無符號整型(范圍從 0 到 255) |
uint16 |
無符號整型(范圍從 0 到 65535) |
uint32 |
無符號整型(范圍從 0 到 4294967295) |
uint64 |
無符號整型(范圍從 0 到 18446744073709551615) |
float_ |
float64 的簡化形式 |
float16 |
半精度浮點型:符號比特位,5 比特位指數(exponent),10 比特位尾數(mantissa) |
float32 |
單精度浮點型:符號比特位,8 比特位指數,23 比特位尾數 |
float64 |
雙精度浮點型:符號比特位,11 比特位指數,52 比特位尾數 |
complex_ |
complex128 的簡化形式 |
complex64 |
復數,由兩個 32 位浮點數表示 |
complex128 |
復數,由兩個 64 位浮點數表示 |
上表列出了標准 NumPy 數據類型。請注意,當構建一個數組時,你可以用一個字符串參數來指定數據類型:
np.zeros(10, dtype='int16')
或者用相關的 NumPy 對象來指定:
np.zeros(10, dtype=np.int16)
還可以進行更高級的數據類型指定,例如指定高位字節數或低位字節數;更多的信息可以在 NumPy 文檔(http://numpy.org/)中查看。
六、參考
1. 《Python數據科學手冊》 [美] Jake VanderPlas [VanderPlas, Jake]
(完)