資料科學 IPython 筆記本 9.3 理解 Python 中的資料型別
9.3 理解 Python 中的資料型別
本節是《Python 資料科學手冊》(Python Data Science Handbook)的摘錄。
譯者:飛龍
資料驅動的科學和有效計算需要了解資料的儲存和操作方式。本節概述瞭如何在 Python 語言本身中處理資料陣列,以及對比 NumPy 如何改進它。對於理解本書其餘部分的大部分內容,理解這種差異至關重要。
Python 的使用者通常被它的易用性吸引,其中一部分是動態型別。雖然像 C 或 Java 這樣的靜態型別語言要求顯式宣告每個變數,但像 Python 這樣的動態型別語言會跳過此規範。 例如,在 C 中,你可以指定特定操作,如下所示:
/* C 程式碼 */
int result = 0;
for(int i=0; i<100; i++){
result += i;
}
在 Python 中,可以用這種方式編寫等效的操作:
# Python 程式碼
result = 0
for i in range(100):
result += i
注意主要區別:在 C 中,每個變數的資料型別是顯式宣告的,而在 Python 中,型別是動態推斷的。 這意味著,例如,我們可以將任何型別的資料分配給任何變數:
# Python 程式碼
x = 4
x = "four"
這裡我們將x
的內容從整數轉換為字串。 C 中的相同內容會導致編譯錯誤或其他無意義的結果(取決於編譯器設定):
/* C 程式碼 */
int x = 4;
x = "four"; // 失敗
這種靈活性,是使 Python 和其他動態型別語言方便易用的一個方面。理解它的原理,是學習如何有效使用 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 中的單個整數實際上包含四個部分:
ob_refcnt
, 引用計數,幫助 Python 靜默處理記憶體分配和釋放ob_type
, 它編碼變數的型別ob_size
, 它指定以下資料成員的大小ob_digit
, 其中包含我們期望 Python 變量表示的實際整數值。
這意味著在 Python 中儲存整數,與在 C 等編譯語言中的整數相比,存在一些開銷,如下圖所示:
這裡PyObject_HEAD
是結構的一部分,包含引用計數,型別程式碼和之前提到的其他部分。
注意這裡的區別:C 整數本質上是記憶體中位置的標籤,它的位元組編碼整數值。Python 整數是指標,指向記憶體中包含所有 Python 物件資訊的位置,包含編碼整數值的位元組。Python 整數結構中的這些額外資訊,允許 Python 自由動態地編碼。
然而,Python 型別中的所有這些附加資訊都需要付出代價,這在組合了許多這些物件的結構中尤為明顯。
Python 列表不僅僅是列表
現在讓我們考慮,當我們使用包含許多 Python 物件的 Python 資料結構時會發生什麼。
Python 中的標準可變多元素容器就是列表。我們可以建立一個整數列表,如下所示:
L = list(range(10))
L
# [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
type(L[0])
# int
或者,類似地,字串列表:
L2 = [str(c) for c in L]
L2
# ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']
type(L2[0])
# str
由於 Python 的動態型別,我們甚至可以建立異構列表:
L3 = [True, "2", 3.0, 4]
[type(item) for item in L3]
# [bool, str, float, int]
但是這種靈活性需要付出代價:為了允許這些靈活型別,列表中的每個專案都必須包含自己的型別資訊,引用計數和其他資訊 - 也就是說,每個專案都是完整的 Python 物件。在所有變數屬於同一型別的特殊情況下,大部分資訊都是冗餘的:將資料儲存在固定型別陣列中會更加高效。
動態型別列表和固定型別(NumPy 樣式)陣列之間的區別如下圖所示:
在實現級別,陣列基本上包含指向一個連續資料塊的單個指標。
另一方面,Python 列表包含一個指向指標塊的指標,每個指標指向一個完整的 Python 物件,就像我們之前看到的 Python 整數一樣。
同樣,列表的優點是靈活性:因為每個列表元素是包含資料和型別資訊的完整結構,所以列表可以填充為任何所需型別的資料。固定型別的 NumPy 風格陣列缺乏這種靈活性,但是對於儲存和操作資料更有效。
Python 中固定型別的陣列
Python提供了幾種不同的選項,用於在固定型別資料緩衝區中高效儲存資料。內建的array
模組(自 Python 3.3 起可用)可用於建立統一型別的密集陣列:
import array
L = list(range(10))
A = array.array('i', L)
A
# array('i', [0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
這裡'i'
是一個型別程式碼,表示內容是整數。然而,更有用的是 NumPy 包的ndarray
物件。
雖然Python的array
物件提供了基於陣列的,資料的有效儲存,但 NumPy 在陣列上添加了高效操作。我們將在後面的章節中探討這些操作; 在這裡,我們將演示建立 NumPy 陣列的幾種方法。
我們將從別名為np
的標準 NumPy 匯入開始:
import numpy as np
從 Python 列表建立陣列
首先,我們可以使用np.array
從 Python 列表建立陣列:
# 整數陣列
np.array([1, 4, 2, 5, 3])
# array([1, 4, 2, 5, 3])
請記住,與 Python 列表不同,NumPy 僅限於型別相同的陣列。
如果型別不匹配,NumPy 將盡可能向上轉換(此處,整數向上轉換為浮點數):
np.array([3.14, 4, 2, 3])
# array([ 3.14, 4. , 2. , 3. ])
如果我們想顯式設定所得陣列的資料型別,我們可以使用dtype
關鍵字:
np.array([1, 2, 3, 4], dtype='float32')
# array([ 1., 2., 3., 4.], dtype=float32)
最後,與 Python 列表不同,NumPy 陣列可以是顯式多維的; 這是一種方法,使用列表的列表初始化多維陣列:
# 巢狀列表產生多維陣列
np.array([range(i, i + 3) for i in [2, 4, 6]])
'''
array([[2, 3, 4],
[4, 5, 6],
[6, 7, 8]])
'''
內部列表被視為生成的二維陣列的行。
從零開始建立陣列
特別是對於較大的陣列,使用 NumPy 中內建的例程從頭開始建立陣列效率更高。以下是幾個例子:
# 建立長度為 10 的零填充的整數陣列
np.zeros(10, dtype=int)
# array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0])
# 建立一填充的 3x5 浮點數陣列
np.ones((3, 5), dtype=float)
'''
array([[ 1., 1., 1., 1., 1.],
[ 1., 1., 1., 1., 1.],
[ 1., 1., 1., 1., 1.]])
'''
# 建立 3.14 填充的 3x5 浮點數陣列
np.full((3, 5), 3.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]])
'''
# 建立陣列,填充為 0 到 20 步長為 2 的線性序列
# (類似於內建的 range() 函式)
np.arange(0, 20, 2)
# array([ 0, 2, 4, 6, 8, 10, 12, 14, 16, 18])
# 建立五個值的陣列,從 0 到 1 等間隔
np.linspace(0, 1, 5)
# array([ 0. , 0.25, 0.5 , 0.75, 1. ])
# 建立 3x3 陣列,包含 0 到 1 均勻分佈隨機值
np.random.random((3, 3))
'''
array([[ 0.99844933, 0.52183819, 0.22421193],
[ 0.08007488, 0.45429293, 0.20941444],
[ 0.14360941, 0.96910973, 0.946117 ]])
'''
# 建立 3x3 陣列,包含均值為 0 標準差為 1 的正態分佈隨機值
np.random.normal(0, 1, (3, 3))
'''
array([[ 1.51772646, 0.39614948, -0.10634696],
[ 0.25671348, 0.00732722, 0.37783601],
[ 0.68446945, 0.15926039, -0.70744073]])
'''
# 建立 3x3 陣列,包含 [0, 10) 中的隨機值
np.random.randint(0, 10, (3, 3))
'''
array([[2, 3, 4],
[5, 7, 8],
[0, 5, 0]])
'''
# 建立 3x3 單位矩陣
np.eye(3)
'''
array([[ 1., 0., 0.],
[ 0., 1., 0.],
[ 0., 0., 1.]])
'''
# 建立三個整數的未初始化陣列
# 值是記憶體地址中已經存在的任何東西
np.empty(3)
# array([ 1., 1., 1.])
NumPy 標準資料型別
NumPy 陣列包含型別單一的值,因此詳細瞭解這些型別及其限制非常重要。由於 NumPy 是用 C 語言構建的,因此 C,Fortran 和其他相關語言的使用者會熟悉這些型別。標準 NumPy 資料型別列在下表中。
請注意,在構造陣列時,可以使用字串指定它們:
np.zeros(10, dtype='int16')
或者使用相關的 NumPy 物件:
np.zeros(10, dtype=np.int16)
資料型別 | 描述 |
---|---|
bool_ |
布林值(True 或 False)儲存為位元組 |
int_ |
預設整數型別(與 C long 相同;通常是int64 或int32 ) |
intc |
等價於 C int (normally int32 or int64 ) |
intp |
用於索引的整數(與 C ssize_t 相同;通常是int32 或int64 ) |
int8 |
位元組(-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 位指數,10 位尾數 |
float32 |
單精度浮點: 符號位,8 位指數,23 位尾數 |
float64 |
雙精度浮點: 符號位,11 位指數,52 位尾數 |
complex_ |
complex128 的簡寫 |
complex64 |
複數,表示為兩個 32 位浮點 |
complex128 |
複數,表示為兩個 64 位浮點 |
更高階的型別規範是可能的,例如指定大或小端編碼;更多資訊請參閱 NumPy 文件。
NumPy 還支援複合資料型別,這將在結構化資料:NumPy 的結構化陣列中介紹。