[python] [VI coding] 第十二章 數組 - 教學區

[python] [VI coding] 第十二章 數組

文章瀏覽次數 156

特種兵

特種兵圖像(預設)

2021-08-13 15:49:30

From:211.23.21.202

第十二章 數組

本章介紹另一個 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 序列的序列

雖然對於數組的列表討論得比較多,但這些技巧也都適用於列表的列表、數組的數組,甚至列表的數組等序列。

這些稱為序列的序列。在許多情況下有不同種類的序列可以選擇,像是字串、列表與數組等,都可以互相混搭。

應該如何選擇呢?最明顯的,字串是其中限制最多的,它不可變,加上元素都是字元組成,如果想要改變其中的字元,那就應該選擇列表會比較好處理,因為字串只能重新生成一個新字串,而無法單一變更原字串中的字元。

而列表的通用性又優於數組,因為它是可變型態,但一些情況下會更喜歡數組:

  1. 在函數回傳值的 return 語句上,創建一個數組比列表簡單。
  2. 想要定義字典的鍵,那只能選擇不可變的型態,通常就是數組或字串。
  3. 想要作為函數不定參數的傳遞,選擇數組可以防止不小心的意外行為,因為數組不可變。

因為數組不可變的特性,包括排序與倒轉等都不允許,這樣才不會像列表一樣,不小心就被更動。

但如果真的想對數組排序或倒轉呢?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 函數,我們可以清楚地描述出複雜的資料結構。

動動腦

  1. 寫一個名為 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 檔案,請寫程式回答以下問題:

  1. 單字列表中,由 8 個字母組成、內含 a 跟 e 但不能有 u 的單字,總共有幾個?
  2. 單字列表中,請列出符合以下所有條件的單字列表,並在畫面上輸出如底下範例的字母表。
    • 第 3 個字母是 u
    • 倒數第二個字母是 e
    • 不能含有 a 跟 s 字母
    • 首尾字母必須相同但不能是 d 或 r
1 答案
2 答案
...
練習 3 檔名 12-3.py

寫一個陽春的點餐系統,需要完全符合以下執行狀況:

  1. 輸入項目的字母大小寫皆可。
  2. 需要錯誤處理。
  3. 每日特餐列表: 咖哩飯、香嫩堡、超辣麵,需要隨機挑出其中一個,包括價格也要隨機出現(範圍自訂),可使用 choice 函數
  4. 每人一開始都會有 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

謝謝光臨

關於亂數的使用,可參考下面的參考資料。

參考資料

python 數組複習

數組的比較

字串當作函數執行

字串當作函數執行的三種方法

亂數取得

影片

第十二章 數組

最後更新:2021-10-08 20:53:02

From: 111.249.165.250

By: 特種兵