從零開始學Python:第十一課-常用資料結構之列表

千鋒Python唐小強發表於2020-07-02

在開始本節課的內容之前,我們先給大家一個程式設計任務,將一顆篩子擲6000次,統計每個點數出現的次數。這個任務對大家來說應該是非常簡單的,我們可以用1到6均勻分佈的隨機數來模擬擲篩子,然後用6個變數分別記錄每個點數出現的次數,相信大家都能寫出下面的程式碼。


import random


f1 = 0
f2 = 0
f3 = 0
f4 = 0
f5 = 0
f6 = 0
for _ in range( 6000):
   face = random.randint( 1, 6)
    if face == 1:
       f1 += 1
    elif face == 2:
       f2 += 1
    elif face == 3:
       f3 += 1
    elif face == 4:
       f4 += 1
    elif face == 5:
       f5 += 1
    else:
       f6 += 1
print( f'1點出現了 {f1}次')
print( f'2點出現了 {f2}次')
print( f'3點出現了 {f3}次')
print( f'4點出現了 {f4}次')
print( f'5點出現了 {f5}次')
print( f'6點出現了 {f6}次')

看看上面的程式碼,相信大家一定覺得它非常的“笨重”和“醜陋”,更可怕的是,如果要統計擲2顆或者更多的篩子統計每個點數出現的次數,那就需要定義更多的變數,寫更多的分支結構。講到這裡,相信大家一定想問:有沒有辦法用一個變數來儲存多個資料,有沒有辦法用統一的程式碼對多個資料進行操作?答案是肯定的,在Python中我們可以透過容器型別的變數來儲存和操作多個資料,我們首先為大家介紹列表(list)這種新的資料型別。

從零開始學Python:第十一課-常用資料結構之列表

定義和使用列表

在Python中, 列表是由一系元素按特定順序構成的資料序列,這樣就意味著定義一個列表型別的變數, 可以儲存多個資料,而且 允許有重複的資料。跟上一課我們講到的字串型別一樣,列表也是一種結構化的、非標量型別,操作一個列表型別的變數,除了可以使用運算子還可以使用它的方法。

在Python中,可以使用[]字面量語法來定義列表,列表中的多個元素用逗號進行分隔,程式碼如下所示。

items1 = [35, 12, 99, 68, 55, 87]

print(items1)
items2 = [ 'Python', 'Java', 'Go', 'Kotlin']
print(items2)

除此以外,還可以透過Python內建的list函式將其他序列變成列表。準確的說,list並不是一個函式,而是建立列表物件的構造器(後面會講到物件和構造器這兩個概念)。

items1 = list(
range(
1, 
10))

print(items1)    # [ 1, 2, 3, 4, 5, 6, 7, 8, 9]
items2 = list( 'hello')
print(items2)    # [ 'h', 'e', 'l', 'l', 'o']

需要說明的是,列表是一種可變資料型別,也就是說列表可以新增元素、刪除元素、更新元素,這一點跟我們上一課講到的字串有著鮮明的差別。字串是一種不可變資料型別,也就是說對字串做拼接、重複、轉換大小寫、修剪空格等操作的時候會產生新的字串,原來的字串並沒有發生任何改變。

列表的運算子

和字串型別一樣,列表也支援拼接、重複、成員運算、索引和切片以及比較運算,對此我們不再進行贅述,請大家參考下面的程式碼。

items1 = [
35, 
12, 
99, 
68, 
55, 
87]

items2 = [ 45, 8, 29]

# 列表的拼接
items3 = items1 + items2
print(items3)    # [ 35, 12, 99, 68, 55, 87, 45, 8, 29]

# 列表的重複
items4 = [ 'hello'] * 3
print(items4)    # [ 'hello', 'hello', 'hello']

# 列表的成員運算
print( 100 in items3)        # False
print( 'hello' in items4)    # True

# 獲取列表的長度(元素個數)
size = len(items3)
print(size)                 # 9

# 列表的索引
print(items3[ 0], items3[-size])        # 35 35
items3[ -1] = 100
print(items3[size - 1], items3[ -1])    # 100 100

# 列表的切片
print(items3[: 5])          # [ 35, 12, 99, 68, 55]
print(items3[ 4:])          # [ 55, 87, 45, 8, 100]
print(items3[ -5: -7: -1])    # [ 55, 68]
print(items3[:: -2])        # [ 100, 45, 55, 99, 35]

# 列表的比較運算
items5 = [ 1, 2, 3, 4]
items6 = list( range( 1, 5))
# 兩個列表比較相等性比的是對應索引位置上的元素是否相等
print(items5 == items6)    # True
items7 = [ 3, 2, 1]
# 兩個列表比較大小比的是對應索引位置上的元素的大小
print(items5 <= items7)    # True

值得一提的是,由於列表是可變型別,所以透過索引操作既可以獲取列表中的元素,也可以更新列表中的元素。對列表做索引操作一樣要注意索引越界的問題,對於有N個元素的列表,正向索引的範圍是0到N-1,負向索引的範圍是-1到-N,如果超出這個範圍,將引發IndexError異常,錯誤資訊為:list index out of range。

列表元素的遍歷

如果想逐個取出列表中的元素,可以使用for迴圈的,有以下兩種做法。

方法一:

items = [
'Python', 
'Java', 
'Go', 
'Kotlin']


for index in range( len(items)):
    print(items[index])

方法二:


items = [
'Python', 
'Java', 
'Go', 
'Kotlin']


for item in items:
   print(item)

講到這裡,我們可以用列表的知識來重構上面“擲篩子統計每個點數出現次數”的程式碼。


import random


counters = [ 0] * 6
for _ in range( 6000):
   face = random.randint( 1, 6)
   counters[face - 1] += 1
for face in range( 1, 7):
    print(f '{face}點出現了{counters[face - 1]}次')

上面的程式碼中,我們用counters列表中的六個元素分別表示1到6的點數出現的次數,最開始的時候六個元素的值都是0。接下來用隨機數模擬擲篩子,如果搖出1點counters[0]的值加1,如果搖出2點counters[1]的值加1,以此類推。大家感受一下,這段程式碼是不是比之前的程式碼要簡單優雅很多。

列表的方法

和字串一樣,列表型別的方法也很多,下面為大家講解比較重要的方法。

新增和刪除元素

items = [
'Python', 
'Java', 
'Go', 
'Kotlin']


# 使用append方法在列表尾部新增元素
items.append( 'Swift')
print(items)    # [ 'Python', 'Java', 'Go', 'Kotlin', 'Swift']
# 使用 insert方法在列表指定索引位置插入元素
items. insert( 2, 'SQL')
print(items)    # [ 'Python', 'Java', 'SQL', 'Go', 'Kotlin', 'Swift']

# 刪除指定的元素
items. remove( 'Java')
print(items)    # [ 'Python', 'SQL', 'Go', 'Kotlin', 'Swift']
# 刪除指定索引位置的元素
items.pop( 0)
items.pop( len(items) - 1)
print(items)    # [ 'SQL', 'Go', 'Kotlin']

# 清空列表中的元素
items.clear()
print(items)    # []

需要提醒大家,在使用remove方法刪除元素時,如果要刪除的元素並不在列表中,會引發ValueError異常,錯誤訊息是:list.remove(x): x not in list。在使用pop方法刪除元素時,如果索引的值超出了範圍,會引發IndexError異常,錯誤訊息是:pop index out of range。

從列表中刪除元素其實還有一種方式,就是使用Python中的del關鍵字後面跟要刪除的元素,這種做法跟使用pop方法指定索引刪除元素沒有實質性的區別,但後者會返回刪除的元素,前者在效能上略優(del對應位元組碼指令是DELETE_SUBSCR,而pop對應的位元組碼指令是CALL_METHOD和POP_TOP)。

items = [
'Python', 
'Java', 
'Go', 
'Kotlin']

del items[ 1]
print(items)    # [ 'Python', 'Go', 'Kotlin']

元素位置和次數

列表型別的index方法可以查詢某個元素在列表中的索引位置;因為列表中允許有重複的元素,所以列表型別提供了count方法來統計一個元素在列表中出現的次數。請看下面的程式碼。

items = [
'Python', 
'Java', 
'Java', 
'Go', 
'Kotlin', 
'Python']


# 查詢元素的索引位置
print(items.index( 'Python'))       # 0
print(items.index( 'Python', 2))    # 5
# 注意:雖然列表中有 'Java',但是從索引為 3這個位置開始後面是沒有 'Java'
print(items.index( 'Java', 3))      # ValueError: 'Java' is not in list

再來看看下面這段程式碼。

items = [
'Python', 
'Java', 
'Java', 
'Go', 
'Kotlin', 
'Python']


# 查詢元素出現的次數
print(items.count( 'Python'))    # 2
print(items.count( 'Go'))        # 1
print(items.count( 'Swfit'))     # 0

元素排序和反轉

列表的sort操作可以實現列表元素的排序,而reverse操作可以實現元素的反轉,程式碼如下所示。

items = [
'Python', 
'Java', 
'Go', 
'Kotlin', 
'Python']


# 排序
items. sort()
print(items)    # [ 'Go', 'Java', 'Kotlin', 'Python', 'Python']
# 反轉
items. reverse()
print(items)    # [ 'Python', 'Python', 'Kotlin', 'Java', 'Go']

列表的生成式

在Python中,列表還可以透過一種特殊的字面量語法來建立,這種語法叫做生成式。我們給出兩段程式碼,大家可以做一個對比,看看哪一種方式更加簡單優雅。

透過for迴圈為空列表新增元素。

# 建立一個由
19的數字構成的列表

items1 = []
for x in range( 1, 10):
   items1. append(x)
print(items1)

# 建立一個由 'hello world'中除空格和母音字母外的字元構成的列表
items2 = []
for x in 'hello world':
    if x not in ' aeiou':
       items2. append(x)
print(items2)

# 建立一個由個兩個字串中字元的笛卡爾積構成的列表
items3 = []
for x in 'ABC':
    for y in '12':
       items3. append(x + y)
print(items3)

透過生成式建立列表。

# 建立一個由
19的數字構成的列表

items1 = [x for x in range( 1, 10)]
print(items1)    # [ 1, 2, 3, 4, 5, 6, 7, 8, 9]

# 建立一個由 'hello world'中除空格和母音字母外的字元構成的列表
items2 = [x for x in 'hello world' if x not in ' aeiou']
print(items2)    # [ 'h', 'l', 'l', 'w', 'r', 'l', 'd']

# 建立一個由個兩個字串中字元的笛卡爾積構成的列表
items3 = [x + y for x in 'ABC' for y in '12']
print(items3)    # [ 'A1', 'A2', 'B1', 'B2', 'C1', 'C2']

下面這種方式不僅程式碼簡單優雅,而且效能也優於上面使用for迴圈和append方法向空列表中追加元素的方式。可以簡單跟大家交待下為什麼生成式擁有更好的效能,那是因為Python直譯器的位元組碼指令中有專門針對生成式的指令(LIST_APPEND指令);而for迴圈是透過方法呼叫(LOAD_METHOD和CALL_METHOD指令)的方式為列表新增元素,方法呼叫本身就是一個相對耗時的操作。對這一點不理解也沒有關係,記住“ 強烈建議用生成式語法來建立列表”這個結論就可以了。

巢狀的列表

Python語言沒有限定列表中的元素必須是相同的資料型別,也就是說一個列表中的元素可以任意的資料型別,當然也包括列表。如果列表中的元素又是列表,那麼我們可以稱之為巢狀的列表。巢狀的列表可以用來表示表格或數學上的矩陣,例如:我們想儲存5個學生3門課程的成績,可以定義一個儲存5個元素的列表儲存5個學生的資訊,而每個列表元素又是3個元素構成的列表,分別代表3門課程的成績。但是,一定要注意下面的程式碼是有問題的。


scores = [[
0] * 
3] * 
5

print(scores)     # [[0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0]]

看上去我們好像建立了一個5 * 3的巢狀列表,但實際上當我們錄入第一個學生的第一門成績後,你就會發現問題來了,我們看看下面程式碼的輸出。

# 巢狀的列表需要多次索引操作才能獲取元素

scores[ 0][ 0] = 95
print(scores)    # [[95, 0, 0], [95, 0, 0], [95, 0, 0], [95, 0, 0], [95, 0, 0]]

我們不去過多的解釋為什麼會出現這樣的問題,如果想深入研究這個問題,可以透過Python Tutor網站的視覺化程式碼執行功能,看看建立列表時計算機記憶體中發生了怎樣的變化,下面的圖就是在這個網站上生成的。建議大家不去糾結這個問題,現階段只需要記住不能用[[0] * 3] * 5]這種方式來建立巢狀列表就行了。那麼建立巢狀列表的正確做法是什麼呢,下面的程式碼會給你答案。

scores = [
[0] * 3 for _ in range(5)]

scores[ 0][ 0] = 95
print(scores)    # [[95, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0]]
從零開始學Python:第十一課-常用資料結構之列表

在講完下節課的知識點後,我們會把這個案例寫得更為完整一些,實現錄入5個學生3門課程的成績,統計出每個學生和每門課程的平均分。

簡單的總結

Python中的列表底層是一個可以動態擴容的陣列,列表元素在記憶體中也是連續儲存的,所以可以實現隨機訪問(透過一個有效的索引獲取到對應的元素且操作時間與列表元素個數無關)。我們暫時不去觸碰這些底層儲存細節以及列表每個方法的漸近時間複雜度(執行這個方法耗費的時間跟列表元素個數的關係),等需要的時候再告訴大家。現階段,大家只需要知道 列表是容器,可以 儲存各種型別的資料可以透過索引操作列表元素就可以了。


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69923331/viewspace-2702100/,如需轉載,請註明出處,否則將追究法律責任。

相關文章