第十二章 數組
本章介紹另一個 python 的內建複合型態──數組 (tuple)。有人翻譯成元組或數對等,都是指同一種資料型態。
如何讓數組、列表與字典一起運作,也是本章重點之一。
但暫時不探究得太過深入,原因是擔心大家剛接觸這些複合型態,還不夠熟悉,所以目標先放在對於數組有正確的認識與觀念。等到更熟悉後,再回頭檢視三者之間的搭配應用。如果一時間還無法完全讀懂本章全部內容,請不要太過擔心。
補充一件有趣的事,目前對於 tuple 這個單字的發音尚無定論,有人傾向把 u 唸成 supple 的 u 發音,但多數的工程師喜歡唸成 too 的發音。
12-1 數組是不可變的
數組是有序的序列,裡面的元素可以是任何的基本型態,也可以像列表一樣使用數字的索引來取值。
所以數組跟列表有很多相似之處,但最重要的不同點在於數組是不可變的型態,就像字串那樣。
不過不要小看它,一開始會以為很難應用,因為不可變,直覺上會認為沒有列表那麼方便。
其實兩者使用的時機不同,請仔細讀完本章,相信會有不同的看法。
語法上,數組是使用 , 逗號來分隔不同元素的:
>>> t = 'a', 'b', 'c', 'd', 'e'
習慣上會把數組放在 () 一對小括號中,這樣看起來語法似乎會比較完整,但並沒有強制規定。
>>> t = ('a', 'b', 'c', 'd', 'e')
有件重要的事要提醒各位,當數組內只有一個元素的情況,記得最後一定要加上一個逗號才行,不管有沒有小括號都要這麼做。
其實不管數組有多少個元素,最後都加個逗號,不但不會錯,而且還是個好習慣,在只有一個元素時也不會產生錯誤。
>>> t1 = 'a',
>>> type(t1)
<class 'tuple'>
只有一個元素又沒有加上逗號的就不是數組,以上例而言,不加逗號變成字串。
>>> t2 = ('a')
>>> type(t2)
<class 'str'>
對 python 來說,一個元素加上小括號只是優先權的改變而已,並不會改變其原本型態。以上例而言,還是字串。
我們也可以使用內建的函數 tuple() 來建立一個空數組:
>>> t = tuple()
>>> t
()
使用 tuple() 函數還可以建立一個初始化有序的數組:
>>> t = tuple('lupins')
>>> t
('l', 'u', 'p', 'i', 'n', 's')
因為 tuple 是數組的名稱,所以避免使用這個名字來當作變數名稱,免得混淆。
就像列表可以用 []
來初始化,字典可以用 {}
來初始化,數組也可以用 ()
來初始化:
>>> t = ()
>>> type(t)
<class 'tuple'>
大多數用在列表的運算子也可以用在數組上,像是利用中括號的索引來取值:
>>> t = ('a', 'b', 'c', 'd', 'e',)
>>> t[0]
'a'
利用切片運算子選擇一定的片段也沒問題:
>>> t[1:3]
('b', 'c')
但如果想要直接改變數組裡元素的值,則會得到一個錯誤訊息:
>>> t[0] = 'A'
TypeError: object doesn't support item assignment
# 型態錯誤:物件不能支援等號運算子
別忘了這一節的主題,因為數組是不可變的,所以不能這樣做,但可以取代或重新連接成一個新數組,這個特性跟字串一樣:
>>> t = ('A',) + t[1:]
>>> t
('A', 'b', 'c', 'd', 'e')
上面的敘述產生了一個新數組,並把 t 指向它。
那麼比較運算子呢?就跟其他有序型態一樣,先從最左邊的元素開始比較,多出來的元素會被忽略。
>>> (0, 1, 2,) < (1, 3, 4,)
True
>>> (1, 1, 2000000,) < (2, 3, 4,)
True
由上例可以看出,比較為真時就會停止比較,並返回結果。
12-2 數組的賦值
在寫程式時經常會遇到互相替換兩個變數值的情況,普遍的做法是用一個暫存變數來當作兩者之間的橋樑,以進行變更,轉換 a 與 b 的例子:
>>> temp = a
>>> a = b
>>> b = temp
這種寫法有點麻煩,看起來也不那麼直覺,使用數組有更簡便的寫法:
>>> a, b = b, a
一行就完成的寫法,整個算式在還沒結束前,就把右邊式子的值給了左邊,還少用了一個變數,要注意等號兩邊的元素數量必須相等:
>>> a, b = 1, 2, 3
ValueError: too many values to unpack
# 數值錯誤:過多的數值無法對應
通常等號右邊可以是任何序列,像是字串、列表或數組,如果打算從電子信箱分割出使用者帳號與網域名稱,可以這樣寫:
>>> addr = 'monty@python.org'
>>> uname, domain = addr.split('@')
因為右邊的算式剛好可以分割出兩個字串,所以分別給了左邊的變數:
>>> uname
'monty'
>>> domain
'python.org'
12-3 作為返回值的數組
嚴格來說,一個函數只能返回一個值,但如果返回的是一個數組,就相當於可以一次返回多個值。
舉例來說,當兩個數相除時,若想要同時得到商與餘數,該怎麼做?還記得之前的習題嗎?關於找回零錢的問題。
有一個內建的函數叫做 divmod ,它就可以回傳一個數組,裡面包括商與餘數:
>>> t = divmod(7, 3)
>>> t
(2, 1)
>>> type(t)
<class 'tuple'>
或者使用等號來分配數組內的元素,分別給兩個變數:
>>> quot, rem = divmod(7, 3)
>>> quot
2
>>> rem
1
有一個自定義的函數範例,它返回一個數組:
def minMax(t):
return min(t), max(t)
上面的例子,minMax 利用內建的 min 與 max 函數,分別找出最小與最大值,然後以數組的方式回傳。
12-4 數組參數的數量
還記得之前與大家討論過的函數參數吧?那時介紹了參數與引數的差異、參數的預設值、手動指定參數順序等。
事實上函數可以接收不定數量的引數,以 * 星號來表示,會將這些引數放到一個數組當中。
下面的例子,printAll 函數取得傳入不定數量的參數後,印出它們:
def printAll(*args):
print(args)
事實上,收集來的參數不一定要叫做 args 這個名字,只是比較好記憶而已 (arguments),看看函數如何運作:
>>> printAll(1, 2.0, '3')
(1, 2.0, '3')
還記得之前提到的,一個字串還是多個字串的問題嗎?使用加號連接起來的是一個字串,使用逗號分隔的是多個參數,而函數在定義中參數加上星號,則表示把不定數量的參數都打包成一個數組。
談完打包,接著討論解包。以 divmod 函數為例,如果直接傳入數組作為參數,就會發生錯誤:
>>> t = (7, 3)
>>> divmod(t)
TypeError: divmod expected 2 arguments, got 1
# 型態錯誤:divmod 函數至少需要兩個參數,但只收到一個
為什麼會發生錯誤呢?因為 divmod 是接受實體的兩個參數,一個數組是一個參數。其實關鍵在於函數的定義與當下要調用這個函數時的參數寫法。
我們在呼叫 divmod 時,改成用星號的方式讓 divmod 可以對 t 解包,就沒問題了:
>>> t = (7, 3)
>>> divmod(*t)
(2, 1)
許多內建函數都有相類似的不定參數設計,像是 max 與 min 函數:
>>> max(1, 2, 3)
3
>>> t = (1, 2, 3)
>>> max(t)
3
但是 sum() 就沒那麼靈活:
>>> sum(1, 2, 3)
TypeError: sum expected at most 2 arguments, got 3
# 型態錯誤:sum 最多可以有兩個引數,但目前拿到三個
>>> sum(t)
3
12-5 列表與數組
你曾經想過在一個迴圈裡同時遍歷兩個變數嗎?例如在讀取檔案時,一個變數代表行號,另一個代表該行內容,希望在一個迴圈裡就可以同時顯示這兩項資訊給使用者。
zip 是一個內建的函數,可以同時讓帶入的參數交錯,這個字是拉鍊 zipper 的縮寫,就像拉鍊一樣,一次同時有兩排鍊齒在運作。
以下這個例子同時遍歷一個字串與一個列表:
>>> s = 'abc'
>>> t = [0, 1, 2]
>>> zip(s, t)
<zip object at 0x7f7d0a9e7c48>
結果是一個 zip 物件,這個物件通常用在迴圈裡,幸運的是可以遍歷它:
>>> for pair in zip(s, t):
... print(pair)
...
('a', 0)
('b', 1)
('c', 2)
zip 物件是一種迭代器,迭代器是一種序列,它類似列表,但不能使用索引方式取值。
如果真的想直接使用列表方法,那可以將迭代器轉換成列表:
>>> list(zip(s, t))
[('a', 0), ('b', 1), ('c', 2)]
結果是一個數組的列表,每個數組是列表的一個元素,裡面包括字串與數字,而它們整個是一個列表。
如果序列的長度不同,多餘的就不處理。
>>> list(zip('Anne', 'Elk'))
[('A', 'E'), ('n', 'l'), ('n', 'k')]
你可以同時使用兩個變數來遍歷這個列表:
t = [('a', 0), ('b', 1), ('c', 2)]
for letter, number in t:
print(number, letter)
每次循環都會同時顯示數組裡的數字與字元,輸出如下:
0 a
1 b
2 c
善用 zip 搭配迴圈,我們可以同時遍歷多個數組或列表,設計出一些有用的程式典型。
這裡有一個 hasMatch 函數,一次遍歷 t1 與 t2 兩個變數,而當 t1[i] == t2[i]
時,回傳 True:
def hasMatch(t1, t2):
for x, y in zip(t1, t2):
if x == y:
return True
return False
如果想遍歷一個序列,並且可以自動產生索引,可以使用 enumerate 函數:
for index, element in enumerate('abc'):
print(index, element)
結果是成對的物件,除了字元外也包含索引(預設從 0 開始),輸出結果如下:
0 a
1 b
2 c
特別注意參數順序,遍歷後是相反的。
12-6 字典與數組
字典有一個內建的方法叫做 items,它會返回一個有序的鍵值對,那就是一個數組:
>>> d = {'a':0, 'b':1, 'c':2}
>>> t = d.items()
>>> t
dict_items([('c', 2), ('a', 0), ('b', 1)])
結果是一個 dict_items 物件,類似鍵值對的迭代器,可以像這樣使用迴圈:
>>> for key, value in d.items():
... print(key, value)
...
c 2
a 0
b 1
之前提過,在 python 3.7 以前,字典沒有一定的順序,因此每次顯示的結果不一定是從小排到大。
另一方面,可以把數組的列表初始化成一個新字典:
>>> t = [('a', 0), ('c', 2), ('b', 1)]
>>> d = dict(t)
>>> d
{'a': 0, 'c': 2, 'b': 1}
將 dict 與 zip 結合,可以產生一個創建字典的簡潔方法:
>>> d = dict(zip('abc', range(3)))
>>> d
{'a': 0, 'c': 2, 'b': 1}
現在來舉一個使用數組當作字典的鍵的例子。因為 key 只能使用不可變的型態,所以當 key 需要複合型態時,就會選擇數組。
假設現在有一個以姓、名來對應的電話字典,key 是姓與名的數組,值則是電話號碼。
我們可以寫成: directory[last, first] = number
中括號裡是一個姓名數組的鍵,所以可以用迴圈來遍歷:
for last, first in directory:
print(first, last, directory[last, first])
因為鍵就是一個數組,所以我們在迴圈中使用兩個變數來迭代,分別表示姓與名。
在迴圈中印出這個鍵的值,也就是電話號碼。
12-7 序列的序列
雖然對於數組的列表討論得比較多,但這些技巧也都適用於列表的列表、數組的數組,甚至列表的數組等序列。
這些稱為序列的序列。在許多情況下有不同種類的序列可以選擇,像是字串、列表與數組等,都可以互相混搭。
應該如何選擇呢?最明顯的,字串是其中限制最多的,它不可變,加上元素都是字元組成,如果想要改變其中的字元,那就應該選擇列表會比較好處理,因為字串只能重新生成一個新字串,而無法單一變更原字串中的字元。
而列表的通用性又優於數組,因為它是可變型態,但一些情況下會更喜歡數組:
- 在函數回傳值的 return 語句上,創建一個數組比列表簡單。
- 想要定義字典的鍵,那只能選擇不可變的型態,通常就是數組或字串。
- 想要作為函數不定參數的傳遞,選擇數組可以防止不小心的意外行為,因為數組不可變。
因為數組不可變的特性,包括排序與倒轉等都不允許,這樣才不會像列表一樣,不小心就被更動。
但如果真的想對數組排序或倒轉呢?python 也提供內建函數 sorted 與 reversed 來產生一個新的結果,而不變更原始資料內容。這部分之前的章節都有提過。
除錯
列表、字典與數組都是資料結構的例子,在這一章中,我們開始看到複合式資料結構的混合應用,像是數組的列表、字典裡包含數組的鍵與列表的值等,這些都很實用,但也相對容易出現所謂的「模型錯誤」,包括型態、大小或結構上的錯誤。
舉個例子來說,如果期待一個整數列表,收到的卻是一個傳統整數型態,那就會發生問題。
為了除錯,寫了一個名為 structshape 的函數,接受任意型態的參數,並返回關於這個模型的摘要字串。
可以從這裡 下載它的原始碼, 以下是一個簡單的使用範例:
>>> from structshape import structshape
>>> t = [1, 2, 3]
>>> structshape(t)
'list of 3 int'
>>> t2 = [[1,2], [3,4], [5,6]]
>>> structshape(t2)
'list of 3 list of 2 int'
>>> t3 = [1, 2, 3, 4.0, '5', '6', [7], [8], 9]
>>> structshape(t3)
'list of (3 int, float, 2 str, 2 list of int, int)'
>>> s = 'abc'
>>> lt = list(zip(t, s))
>>> structshape(lt)
'list of 3 tuple of (int, str)'
>>> d = dict(lt)
>>> structshape(d)
'dict of 3 int->str'
透過上面 structshape 函數,我們可以清楚地描述出複雜的資料結構。
動動腦
- 寫一個名為 sumAll 的函數,接受任意數量的參數,然後求這些參數的和並回傳。
def sumAll(*s):
sum = 0
for i in s:
sum += i
return sum
t = (2, 3, 1,)
print(sumAll(*t))
# 6
a = 3
b = 9
c = 1
print(sumAll(a, b, c))
# 13
習題
練習 1 檔名 12-1.py
寫一個名為 mostFrequent 的函數,接受一個字串列表與一個排序列表,返回一個以字母出現頻率多到少的新排列字串。
根據統計,字母出現頻率的列表可 參考這個表 ,只要看最後一個表,英文的部分就可以了。
必須先建立一個 a 到 z 的出現頻率字典,因為字典可能是無序的,所以要想辦法排序成由多到少的出現頻率順序,再加以利用。
不可以自己手動建立好由多到少的排列列表,只能用程式處理,可參考下面的連結。
最後輸出要像這樣:
- string -> tinsrg
- dear -> eard
- more -> eorm
練習 2 檔名 12-2.py
利用之前的 words.txt 檔案,請寫程式回答以下問題:
- 單字列表中,由 8 個字母組成、內含 a 跟 e 但不能有 u 的單字,總共有幾個?
- 單字列表中,請列出符合以下所有條件的單字列表,並在畫面上輸出如底下範例的字母表。
- 第 3 個字母是 u
- 倒數第二個字母是 e
- 不能含有 a 跟 s 字母
- 首尾字母必須相同但不能是 d 或 r
1 答案
2 答案
...
練習 3 檔名 12-3.py
寫一個陽春的點餐系統,需要完全符合以下執行狀況:
- 輸入項目的字母大小寫皆可。
- 需要錯誤處理。
- 每日特餐列表: 咖哩飯、香嫩堡、超辣麵,需要隨機挑出其中一個,包括價格也要隨機出現(範圍自訂),可使用 choice 函數。
- 每人一開始都會有 500 元可以使用。
玩第一次
歡迎光臨 python 餐廳
(M) 看菜單
(G) 看錢包
(Q) 要回家
請輸入字母來選擇功能: a
輸入錯誤,請重新輸入
歡迎光臨 python 餐廳
(M) 看菜單
(G) 看錢包
(Q) 要回家
請輸入字母來選擇功能: G
你帶了 500 元在身上
歡迎光臨 python 餐廳
(M) 看菜單
(G) 看錢包
(Q) 要回家
請輸入字母來選擇功能: m
今天的菜單如下
1 print 套餐 100 元
2 if 拉麵 120 元
3 while 燒烤 200 元
S 超辣麵 80 元
C 結帳
Q 回上頁
請輸入數字或字母來點餐: C
您尚未點餐故不需結帳
今天的菜單如下
1 print 套餐 100 元
2 if 拉麵 120 元
3 while 燒烤 200 元
S 超辣麵 80 元
C 結帳
Q 回上頁
請輸入數字或字母來點餐: 22
輸入錯誤,請重新輸入
今天的菜單如下
1 print 套餐 100 元
2 if 拉麵 120 元
3 while 燒烤 200 元
S 超辣麵 80 元
C 結帳
Q 回上頁
請輸入數字或字母來點餐: 2
if 拉麵共 1 份 120 元
今天的菜單如下
1 print 套餐 100 元
2 if 拉麵 120 元
3 while 燒烤 200 元
S 超辣麵 80 元
C 結帳
Q 回上頁
請輸入數字或字母來點餐: Q
歡迎光臨 python 餐廳
(M) 看菜單
(G) 看錢包
(Q) 要回家
請輸入字母來選擇功能: m
今天的菜單如下
1 print 套餐 100 元
2 if 拉麵 120 元
3 while 燒烤 200 元
S 咖哩飯 170 元
C 結帳
Q 回上頁
請輸入數字或字母來點餐: 2
if 拉麵共 1 份 120 元
今天的菜單如下
1 print 套餐 100 元
2 if 拉麵 120 元
3 while 燒烤 200 元
S 咖哩飯 170 元
C 結帳
Q 回上頁
請輸入數字或字母來點餐: s
咖哩飯共 1 份 170 元
今天的菜單如下
1 print 套餐 100 元
2 if 拉麵 120 元
3 while 燒烤 200 元
S 咖哩飯 170 元
C 結帳
Q 回上頁
請輸入數字或字母來點餐: 3
while 燒烤共 1 份 200 元
今天的菜單如下
1 print 套餐 100 元
2 if 拉麵 120 元
3 while 燒烤 200 元
S 咖哩飯 170 元
C 結帳
Q 回上頁
請輸入數字或字母來點餐: 2
if 拉麵共 2 份 240 元
今天的菜單如下
1 print 套餐 100 元
2 if 拉麵 120 元
3 while 燒烤 200 元
S 咖哩飯 170 元
C 結帳
Q 回上頁
請輸入數字或字母來點餐: C
結帳:
if 拉麵 2 份 240 元
while 燒烤 1 份 200 元
咖哩飯 1 份 170 元
共 610 元
因為錢不夠,所以被留下來洗碗
謝謝光臨
玩第二次
歡迎光臨 python 餐廳
(M) 看菜單
(G) 看錢包
(Q) 要回家
請輸入字母來選擇功能: m
今天的菜單如下
1 print 套餐 100 元
2 if 拉麵 120 元
3 while 燒烤 200 元
S 香嫩堡 50 元
C 結帳
Q 回上頁
請輸入數字或字母來點餐: 1
print 套餐 1 份 100 元
今天的菜單如下
1 print 套餐 100 元
2 if 拉麵 120 元
3 while 燒烤 200 元
S 香嫩堡 50 元
C 結帳
Q 回上頁
請輸入數字或字母來點餐: 3
while 燒烤 1 份 200 元
今天的菜單如下
1 print 套餐 100 元
2 if 拉麵 120 元
3 while 燒烤 200 元
S 香嫩堡 50 元
C 結帳
Q 回上頁
請輸入數字或字母來點餐: C
結帳:
print 套餐 1 份 100 元
while 燒烤 1 份 200 元
共 300 元
身上剩下 200 元
歡迎光臨 python 餐廳
(M) 看菜單
(G) 看錢包
(Q) 要回家
請輸入字母來選擇功能: G
你帶了 200 元在身上
歡迎光臨 python 餐廳
(M) 看菜單
(G) 看錢包
(Q) 要回家
請輸入字母來選擇功能: m
今天的菜單如下
1 print 套餐 100 元
2 if 拉麵 120 元
3 while 燒烤 200 元
S 咖哩飯 150 元
C 結帳
Q 回上頁
請輸入數字或字母來點餐: s
咖哩飯 1 份 150 元
今天的菜單如下
1 print 套餐 100 元
2 if 拉麵 120 元
3 while 燒烤 200 元
S 咖哩飯 150 元
C 結帳
Q 回上頁
請輸入數字或字母來點餐: c
結帳:
咖哩飯 1 份 150 元
共 150 元
歡迎光臨 python 餐廳
(M) 看菜單
(G) 看錢包
(Q) 要回家
請輸入字母來選擇功能: q
謝謝光臨
關於亂數的使用,可參考下面的參考資料。
參考資料
影片
最後更新:2021-10-08 20:53:02
From: 111.249.165.250
By: 特種兵