本篇是「對比 Python 學習 Go」 系列的第四篇,本篇文章咱們來看下 Go 的高級數據結構,因文章偏長分爲兩篇,此爲上篇。本系列的其餘文章可到 「對比 Python 學習 Go」- 開篇 查看,下面咱們開始今天的分享。html
Python 數據結構底層徹底依賴解釋器的實現方式,沒有特殊說明文中數據結構對應默認解釋器 CPython。python
從數據結構上來說,有「數組」和「鏈表」兩種基本的數據結構,還有不少基於他們的高級數據結構如棧、隊列、散列表等等。做爲編程語言,Go 和 Python 是如何定義本身的數據結構的呢?根據數據結構的特性,咱們大體能夠將 Go 和 Python 的數據結構分以下幾個類型:git
下邊咱們來逐一介紹。github
數組,它是一個線性表的結構。它有以下特性:golang
回顧了數組的特性,咱們來看下 Go 和 Python 中有哪些類數組的數據結構。編程
在 Go 語言中,有「數組」和「切片」兩個類數組數據結構。c#
數組數組
Go 的數組特性可總結以下:緩存
數組的初始化和操做以下:markdown
package main
import "fmt"
func main() {
// 類型 [n]T是一個有 n個類型爲 T的值的數組
// 先聲明,後賦值
var a [2]string
a[0] = "Hello"
a[1] = "World"
// 聲明時,直接賦值
b := [5]int{10,20,30,40,50}
// 可直接經過下標來訪問數組
fmt.Println(a)
fmt.Println(a[0], a[1])
fmt.Println(b)
fmt.Println(b[1], b[2])
// 經過len()函數可獲取數組長度
fmt.Println(len(a))
fmt.Println(len(b))
// 數組元素賦值
b[1] = 25
fmt.Println(b)
fmt.Println(b[1])
}
複製代碼
除了普通數組以外,還有多維數組,即數組的數組。
// 聲明一個二維整型數組,兩個維度分別存儲 4 個元素和 2 個元素
var arr [4][2]int
arr[0] = [2]int{10, 11}
arr[0][1] = 15
// 聲明時,直接賦值
arr1 := [4][2]int{{10, 11}, {20, 21}, {30, 31}, {40, 41}}
// 聲明時,使用下標賦值
arr2 := [4][2]int{1: {20, 21}, 3: {40, 41}}
arr3 := [4][2]int{1: {0: 20}, 3: {1: 41}}
// 使用數組類初始化數組
var arr4 [2]int = arr1[1]
// 使用數組元素來賦值普通元素
var value int = arr1[1][0]
// 使用索引獲取數組值
fmt.Println(arr)
fmt.Println(arr1)
fmt.Println(arr1[0][1])
fmt.Println(arr2)
fmt.Println(arr3)
fmt.Println(arr4)
fmt.Println(value)
// [[10 15] [0 0] [0 0] [0 0]]
// [10 15]
// [[10 11] [20 21] [30 31] [40 41]]
// 11
// [[0 0] [20 21] [0 0] [40 41]]
// [[0 0] [20 0] [0 0] [0 41]]
// [20 21]
// 20
複製代碼
數組中未被初始化的元素,會自動初始化爲各種型對應的「零值」。
切片
Go 的數組長度是固定的,內存空間中存儲了數據值,當存儲的量特別大的時候,使用起來極不方便。在 Go 語言中,除了數組還定義了一個類數組結構,叫作「切片(slice)」。
切片是數組段的描述符。它由一個指向底層數組的指針,數組段長度及其容量(段的最大長度)組成。切片更像是數組的引用類型,而數組則是值類型。其結構以下:
在 Go 的編程中,因爲數組的各類侷限性,對數據集合類型的操做處理時,切片是首選的數據結構。切片的底層數據存儲仍是數組,因此數組的一些特性切片也有。
// 切片使用make(slice, len, cap) 聲明, cap可省略,省略時,等於len
sli := make([]int, 2, 3)
fmt.Println(sli) // 未賦值時,各元素爲對應的零值
fmt.Printf("%p %v %v \n", sli, len(sli), cap(sli))
// 聲明時,初始化
nums := []int{10, 20, 30, 40}
fmt.Println(nums)
fmt.Printf("%p %v %v \n", nums, len(nums), cap(nums))
// nil 切片,指針爲空,長度和容量爲0
var nums1 []int
fmt.Println(nums1)
// 空切片,指針爲指向一個空數組,長度和容量爲0
var nums2 = make([]int, 0)
nums3 := []int{}
fmt.Println(nums2)
fmt.Println(nums3)
nums[0] = 12
fmt.Println(nums)
// 從切片建立切片
fmt.Println(nums)
fmt.Println(nums[1:2]) // 長度爲2-1 =1,容量1
fmt.Println(nums[1:2:4]) // 長度爲2-1=1,容量爲4-1=3
//slice[i:] // 從 i 切到最尾部
//slice[:j] // 從最開頭切到 j(不包含 j)
//slice[:] // 從頭切到尾,等價於複製整個 slice
// 切片的追加, 使用內建函數 append(src, item) 返回 新的切片
nums = append(nums, 10) // 添加一個
nums = append(nums, 10, 20) // 同時添加多個
newNums := append(nums, nums1...) // 合併兩個切片
fmt.Println(newNums)
fmt.Println(nums)
// 切片的複製, 使用內建函數 copy(dst, src) 返回複製的元素個數
// 賦值時,接收的切片容量須要大於原切片,不然複製失敗,且不會報錯
copyNums := make([]int, 5)
count := copy(copyNums, nums)
fmt.Println(count)
fmt.Println(copyNums)
fmt.Println(nums)
複製代碼
上面說到,Go 的切片歸根接地是一個數據段的描述符。底層是引用的數組結構,當多個切片同時引用一個數組時,使用下標來修改切片,則會相互的影響,使用時,必定要注意。
// 數組共享的切片
sli := make([]int, 3) // 定義一個長度,大小都爲3的切片
sli1 := sli[:2] // 由切片再建立切片,sli 和sli1 底層引用同一個數組
// slice: 0xc0000160c0 ,len: 3 ,cap: 3
fmt.Printf("slice: %p ,len: %v ,cap: %v \n", sli, len(sli), cap(sli))
// slice1: 0xc0000160c0 ,len: 2 ,cap: 3
fmt.Printf("slice1: %p ,len: %v ,cap: %v \n", sli1, len(sli1), cap(sli1))
sli[0] = 10
fmt.Println(sli) // [10 0 0]
fmt.Println(sli1) // [10 0]
複製代碼
切片的容量,可在使用中動態增加。切片的動態增加是經過內置函數 append()
來實現的,它會自動的處理好切片擴縮容的全部細節。擴容長度一般是原來切片容量的一倍,當容量大於 1024 時,增加變爲原來容量的 1/4 倍。
// 切片擴容
fmt.Printf("%p %v %v \n", sli, len(sli), cap(sli)) // 0xc0000160c0 3 3
sli = append(sli, 1)
sli = append(sli, 2)
fmt.Println(sli) // [10 0 0 1 2]
fmt.Printf("%p %v %v \n", sli, len(sli), cap(sli)) // 0xc00001a0c0 5 6
複製代碼
上邊代碼中 sli 長度爲 3,容量爲 3。append 元素以後,由於容量已經被佔滿,因此自動擴容了一倍的容量,通過兩次 append 以後,長度變爲 5,容量爲 6。除了長度和容量外,你們可能發現了,數組地址變了,這說明 append 函數在擴容的時候,會建立一個新的底層數組,而並不是在原數組上進行直接追加擴容。
append 函數擴容建立新數組,這時候再經過下標來修改數據或繼續執行 append 是不會覆蓋原底層數組的,由於已經不是一個數組了。這個特色常常被用在從一個切片建立另外一個切片時,防止切片賦值相互影響。
sli := make([]int, 3) // 定義一個長度,大小都爲3的切片
fmt.Println(sli) // [0 0 0]
sli1 := sli[:3:3] // sli1 長度3-0=3,容量爲3-0=3
fmt.Println(sli1) // [0 0 0]
fmt.Printf("%p %v %v \n", sli, len(sli), cap(sli)) // 0xc0000160c0 3 3
fmt.Printf("%p %v %v \n", sli1, len(sli1), cap(sli1)) // 0xc0000160c0 3 3
sli1 = append(sli1, 2) // 容量已滿,新建立底層數組
fmt.Printf("%p %v %v \n", sli1, len(sli1), cap(sli1)) // 0xc00001a0c0 4 6
複製代碼
Python 中的類數組數據結構,爲列表(List)和元組(tuple)。
List
列表,與數組相比,更爲高級,列表的底層結構以下:
typedef struct {
PyObject_VAR_HEAD
PyObject **ob_item;
Py_ssize_t allocated;
} PyListObject;
複製代碼
拋去公共的頭部變量PyObject_VAR_HEAD
,列表由一個指針數組ob_item
,和一個容量allocated
組成。結構以下:
列表中,並未存儲實際的數值,而是存儲了數值的引用地址,引用地址能夠指向任意類型的數據,也就能夠理解爲何列表中能夠有任意類型的元素了。另外一個,列表中引用地址的大小相同,保存在一個連續的存儲空間,也就有了數組的一些特性,能夠經過下標快速定位。
根據列表底層結構和 Python 官方文檔 How are lists implemented in CPython? 總結 List 有以下特性:
列表的初始化及操做以下:
# 列表的初始化
l1 = [] # 推薦,更快速
l2 = list()
l3 = [1,2,3,4]
# 列表相加
print(l1+l2)
print(l1*2) # 可以使用*來複制列表
l3 += l1
print(l3)
# 列表取值
print(l1[2])
print(l3[1:2]) # 從下標1到下標2
print(l3[1:]) # 到結尾
print(l3[:2]) # 從0開始
# 列表的長度
print(len(l1))
# 刪除索引爲3的元素
del l1[3]
複製代碼
Python 的列表做爲內建的高級數據結構,實現了一系列的操做功能函數。
dir(list)
['__add__', '__class__', '__contains__', '__delattr__', '__delitem__', '__dir__', '__doc__',
'__eq__', '__format__', '__ge__', '__getattribute__', '__getitem__', '__gt__', '__hash__',
'__iadd__', '__imul__', '__init__', '__init_subclass__', '__iter__', '__le__', '__len__',
'__lt__', '__mul__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__reversed__',
'__rmul__', '__setattr__', '__setitem__', '__sizeof__', '__str__', '__subclasshook__',
'append', 'clear', 'copy', 'count', 'extend', 'index', 'insert', 'pop', 'remove', 'reverse', 'sort']
l1 = [1, 2, 3, 4, 5]
l2 = [6, 7, 8, 9, 10]
# 列表追加
l3 = l1.append(6)
# 列表合併
l4 = l1.extend(l2)
# 列表元素的索引
print(l1.index(2)) # 2在l1列表中的索引
# 插入列表元素
print(l1) # [1, 2, 3, 4, 5, 6, 6, 7, 8, 9, 10]
l1.insert(2, 12) # 在索引爲2的位置插入值12,原值及後邊的值後移。
print(l1) # [1, 2, 12, 3, 4, 5, 6, 6, 7, 8, 9, 10]
# 彈出列表指定索引的元素
num = l1.pop() # 默認爲最大索引
print(num) # 10
print(l1) # [1, 2, 12, 3, 4, 5, 6, 6, 7, 8, 9]
num = l1.pop(3) # 彈出索引爲3的元素,後邊的元素前移
print(l1) # [1, 2, 12, 4, 5, 6, 6, 7, 8, 9]
# 刪除指定值的元素
print(l1) # [1, 2, 12, 4, 5, 6, 6, 7, 8, 9]
l1.remove(2) # 刪除值爲2的元素, 後邊的元素前移
print(l1) # [1, 12, 4, 5, 6, 6, 7, 8, 9]
# 清空列表
print(l1) # [1, 12, 4, 5, 6, 6, 7, 8, 9]
l1.clear() # 清空列表l1
print(l1) # []
# 列表排序
print(l2) # [6, 7, 8, 9, 10]
l2.sort()
print(l2) # [6, 7, 8, 9, 10]
l2.reverse()
print(l2) # [6, 7, 8, 9, 10]
複製代碼
除了列表自帶的一些操做,還適用一些內建的函數。
# sorted 排序函數
print(l2)
l21 = sorted(l2, key=lambda x: x, reverse=False)
print(l21)
複製代碼
元組
Python 中除了列表,還有元組比較像數組。元組和列表類似,只是不能增長、刪除、修改。底層結構以下:
typedef struct {
PyObject_VAR_HEAD
PyObject *ob_item[1];
} PyTupleObject;
複製代碼
除了頭字段,只有一個指針數組。沒有想列表同樣的容量字段allocated
,這正映了元組的不可變特性。除了元素的不可變特性外,其餘和列表同樣,是列表類型的一個子集。
你可能會有這樣的疑問,都有列表了,元組存在的意義在哪裏?元組相比於列表,有如下幾點優點:
元組的初始化及經常使用操做:
# 元組的初始化
a = (1, 2, 3)
b = ('1', [2, 3])
c = ('1', '2', (3, 4))
d = ()
e = (1,) # 元組中只有一個元素時,須要使用逗號結尾
print(a, b, c, d, e)
# (1, 2, 3) ('1', [2, 3]) ('1', '2', (3, 4)) () (1,)
# 下標獲取值
print(a[1]) # 2
# 元組合並
print(a+b) # (1, 2, 3, '1', [2, 3])
# 內建函數使用
# 元組長度
print(len(a)) # 3
# 使用 * 是複製指針
f = a*2
print(f) # (1, 2, 3, 1, 2, 3)
print(id(f[0])) # 4376435920
print(id(a[0])) # 4376435920
print(id(f[3])) # 4376435920
# 沒法更新編輯
# a[0] = 1
# Traceback (most recent call last):
# File "/Users/deanwu/projects/01_LearnDocs/learn_codes/python/python_list.py", line 15, in <module>
# a[0] = 1
# TypeError: 'tuple' object does not support item assignment
# 沒法刪除
# del a[0]
# Traceback (most recent call last):
# File "/Users/deanwu/projects/01_LearnDocs/learn_codes/python/python_list.py", line 21, in <module>
# del a[0]
# TypeError: 'tuple' object doesn't support item deletion
複製代碼
本篇咱們我學習了 Go 和 Python 的高級數據結構中類數組的結構,它們都有一些數組的特性,但又都有本身語言的特色。Go 的切片和 Python 的列表,底層都基於數組,但 Go 的切片更像是數組的描述符指針,而 Python 的列表,則是使用地址和數據分開存儲,引用地址使用連續空間存儲,繼承數組快速查找的優勢,外部存儲實現任意類型元素存儲。總體下來,甚是巧妙。
無論何種語言,咱們在使用時,既要了解結構的基本用法,還要了解其底層的邏輯結構,才能避免在使用時的一些莫名的坑。
我是DeanWu,一個努力成爲真正SRE的人。
關注公衆號「碼農吳先生」, 可第一時間獲取最新文章。回覆關鍵字「go」「python」獲取我收集的學習資料,也可回覆關鍵字「小二」,加我wx拉你進技術交流羣,聊技術聊人生~