1. 程式人生 > >資料科學 IPython 筆記本 9.3 理解 Python 中的資料型別

資料科學 IPython 筆記本 9.3 理解 Python 中的資料型別

9.3 理解 Python 中的資料型別

本節是《Python 資料科學手冊》(Python Data Science Handbook)的摘錄。

譯者:飛龍

協議:CC BY-NC-SA 4.0

資料驅動的科學和有效計算需要了解資料的儲存和操作方式。本節概述瞭如何在 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 等編譯語言中的整數相比,存在一些開銷,如下圖所示:

Integer Memory Layout

這裡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 樣式)陣列之間的區別如下圖所示:

Array Memory Layout

在實現級別,陣列基本上包含指向一個連續資料塊的單個指標。

另一方面,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相同;通常是int64int32
intc 等價於 C int(normally int32 or int64
intp 用於索引的整數(與 C ssize_t相同;通常是int32int64
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 的結構化陣列中介紹。