2017年5月9日 星期二

MicroPython on ESP8266 (四) : 字典與集合型別測試

過去兩周對 ESP8266 上的 MicroPython 進行了初步的測試, 發現能在 ESP-01 模組上執行 Python 的感覺真好, 因為 Python 實在太好用了. 今天要下午才進辦公室, 所以早上就來測試最後的兩種容器型別 : 字典 dict 與集合 set, 這兩種型別的資料都是用大括弧 {} 包起來, 它們的元素或項目也都是無順序的.

Python 常用資料型別的繼承圖如下 :


此系列前面三篇測試參考 :

MicroPython on ESP8266 (二) : 數值型別測試

一. 字典 dict :

Python 的字典與 Javascript 或 PHP 的關聯式陣列很類似, 是由 {鍵:值} 對 (key-value pair) 組成的多值資料結構, 使用獨一無二 (不可重複) 的鍵當索引來定位資料 (值). 鍵通常使用整數或字串等不可變型別資料, 而值則可以是任何型別資料.

理論上任何 Python 不可變型別的資料 (布林值, 整數, 浮點數, 元組, 字串) 都可以用作字典的鍵, 但因為布林值當鍵使用場合不多; 浮點數因無法精確表示, 所以也不適合拿來當鍵; 而元組的元素若為串列 (可變) 也不可以當鍵, 因此實際上最常用作鍵的是整數與字串.

不可變型別如 int, float, str, tuple 才能用來當作鍵的原因是這些型別的資料是可雜湊的 (hashable), 因為它們都繼承自 Hashable 這個抽象型別; 而可變型別如 list, dict, set 則是不可雜湊的. 所謂可雜湊是指可以利用雜湊演算法將資料進行壓縮與摘要, 算出一個獨一無二可代表該資料的的數字, 此數值稱為其雜湊值, 又稱為其數字指紋, 這獨一無二的特性正是可做為 key 的必要條件. Python 有一個內建函數 hash() 可計算雜湊值, 例如 :

 >>> hash(0)
0
>>> hash(1)
1
>>> hash(2)
2
>>> hash(3)
3
>>> hash(True)    #可見 True 就是整數 1
1
>>> hash(False)   #可見 False 就是整數 0
0
>>> hash('a')
46532
>>> hash('b')
46535
>>> hash('c')
46534
>>> hash('a')
46532
>>> hash('abc')
12933
>>> hash(1.0)
1
>>> hash(1.0001)
1
>>> hash(1.2)
1
>>> hash(1.99)
1

浮點數雖然可雜湊, 但由於大部分的浮點數無法精確表示, 所以實務上很少拿來做鍵. 上面範例中的浮點數在 MicroPython 實作的雜湊演算法算起來都是 1. 但是在 CPython 執行結果不同 :

Microsoft Windows [版本 10.0.14393]
(c) 2016 Microsoft Corporation. 著作權所有,並保留一切權利。

C:\Users\Tony>python
Python 3.6.1 (v3.6.1:69c0db5, Mar 21 2017, 18:41:36) [MSC v.1900 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> hash(1.0)
1
>>> hash(1.0001)
230584300921345
>>> hash(1.2)
461168601842738689
>>> hash(1.99)
2282784579121556993

不像序列型別是依序排列存放, 可以用索引定位出資料的位置, 字典與集合的資料是無順序的, 無法用索引存取, 而是靠鍵的雜湊值當作參考去定位出資料的正確的位址.


1. 建立字典 : 

建立字典的第一種方法是使用大括號包住以逗號分隔的鍵:值對, 例如 :

>>> user={'name':'Peter', 'age':16, 'gender':'male'}
>>> user
{'name': 'Peter', 'age': 16, 'gender': 'male'}          #以字串當鍵
>>> type(user)
<class 'dict'>
>>> ascii={61:'a',62:'b',63:'c',64:'d'}  #以整數當鍵
>>> ascii
{64: 'd', 61: 'a', 62: 'b', 63: 'c'}
>>> constants={3.14:'pi',2.718:'e'}       #以浮點數當鍵
>>> constants
{2.718: 'e', 3.14: 'pi'}

注意, 浮點數雖然可以當鍵, 但由於 MicroPython 的浮點數只能表示到小數後五位, 如果用浮點數當鍵會變成無實用價值, 例如 :

>>> a={3.14:'pi1',3.14159:'pi2',3.1415926:'pi3'}
>>> a
{3.14: 'pi1', 3.14159: 'pi2', 3.14159: 'pi3'}   #MicroPython 只能表示到小數後五位
>>> a[3.14]                                   #結果看起來竟然有兩個相同的鍵 3.14159
'pi1'
>>> a[3.14159]                             #事實上只要知道真正的鍵, 還是可以正確取得值
'pi2'
>>> a[3.1415926]
'pi3'

建立字典的第二種方法是先建立一個空字典, 再用賦值方式新增字典的元素, 字典的存取使用中括號 [key], 而對鍵賦值 dict_name[key]=value 即是建立一個鍵:值對, 例如 :

>>> user={}                        #建立一個空字典
>>> user['name']='Peter'     #指定鍵-值對
>>> user['age']=16              #指定鍵-值對
>>> user['gender']='male'    #指定鍵-值對
>>> user
{'gender': 'male', 'name': 'Peter', 'age': 16}     #按照字串鍵字母排序, 小寫先, 大寫後
>>> constants={}
>>> constants[3.14]='pi'
>>> constants[2.718]='e'
>>> constants
{2.718: 'e', 3.14: 'pi'}                             #按照數值鍵的大小升冪排序
>>> a={}
>>> a['a']=61
>>> a['b']=62
>>> a['A']=41
>>> a['B']=42
>>> a
{'a': 61, 'A': 41, 'B': 42, 'b': 62}              #按照字串鍵字母排序, 小寫先, 大寫後

可見使用這種方式建立的字典, 其鍵:值對並非按照建立的順序排列, 而是依照 key 來排序. 鍵若為數值, 則依鍵之升冪排序; 鍵若為字串, 則依鍵的字母排序, 小寫先, 大寫後.

建立字典物件的第三種方法是利用 dict() 建構式 (內建函數), 它可以將大於 1 對的對偶物件轉成 dict 物件, 這個對偶物件可以 tuple 或 list, 例如 :

>>> a=((1,'one'),(2,'two'),(3,'three'))     #tuple 的 tuple
>>> dict(a)
{1: 'one', 2: 'two', 3: 'three'}
>>> a=[(1,'one'),(2,'two'),(3,'three')]     #tuple 的 list
>>> dict(a)
{1: 'one', 2: 'two', 3: 'three'}
>>> a=([1,'one'],[2,'two'],[3,'three'])     #list 的 tuple
>>> dict(a)
{1: 'one', 2: 'two', 3: 'three'}
>>> a=[[1,'one'],[2,'two'],[3,'three']]     #list 的 list
>>> dict(a)
{1: 'one', 2: 'two', 3: 'three'}

以上不管是串列的元組, 或元組的串列, 傳入 dict() 後都轉成相同的 dict 物件. 但注意, 如果只有一個對偶物件時, tuple 對偶物件的後面要加一個逗號, 否則會產生錯誤, 這是 tuple 只有單一項目時必須注意之處, 串列無此問題 :

>>> a=((1,'one'))                 #只有一個 tuple 對偶物件項目
>>> dict(a)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'int' object is not iterable  #少一個逗號無法迭代
>>> a=((1,'one'),)                #tuple 只有一個項目時必須以逗號結尾
>>> dict(a)
{1: 'one'}                                             #有結尾逗號順利轉成 dict
>>> a=[(1,'one')]                  #串列只有一個項目時無結尾逗號問題
>>> dict(a)
{1: 'one'}

另外, 項目為雙字元字串的元組或串列也是對偶物件, 傳入 dict() 時會被拆成兩個字元的鍵:值對, 例如 :

>>> a=('a1','b2','c3')    #雙字元字串 'a1', 'b2', 'c3' 被視為對偶
>>> dict(a)
{'a': '1', 'c': '3', 'b': '2'}
>>> a=['a1','b2','c3']  
>>> dict(a)
{'a': '1', 'c': '3', 'b': '2'}

建立字典的第四種方法是用字典物件的 setdefault() 方法, 這方法是為指定的鍵設定預設值, 對於空字典而言, 此方法實際上是新增一個項目, 例如 :

>>> a={}                         #空字典
>>> a.setdefault(1,'a')      #為 key=1 設定預設值為 'a'
'a'
>>> a
{1: 'a'}
>>> a.setdefault(2,'b')
'b'
>>> a.setdefault(3,'c')
'c'
>>> a                                #項目依設定先後順序存放
{1: 'a', 2: 'b', 3: 'c'}
>>> len(a)
3

可見使用 setdefault() 的好處是項目會以設定的先後順序儲存.

建立字典的第四種方法是用字典的類別方法 dict.fromkeys(seq[, default]), 此方法會以傳入的第一參數 (序列型別的串列, 元組, 或字串) 當作鍵, 以第二參數 default 作預設值, 若未傳入第二參數預設為 None. 然後再用指派方式更改項目之值, 例如 :

>>> seq=(1,2,3)                
>>> a=dict.fromkeys(seq)       #以元組為鍵建立字典
>>> a
{3: None, 1: None, 2: None}                  #未傳入第二參數, 預設值為 None
>>> a[1]='a'                             #設值
>>> a[2]='b'
>>> a[3]='c'
>>> a
{3: 'c', 1: 'a', 2: 'b'}
>>> a=dict.fromkeys(seq,'a')    #傳入預設值 'a'
>>> a
{3: 'a', 1: 'a', 2: 'a'}
>>> seq=[1,2,3]
>>> a=dict.fromkeys(seq)        #以串列為鍵建立字典
>>> a
{3: None, 1: None, 2: None}                   #未傳入第二參數, 預設值為 None
>>> seq='123'                           #以字串為鍵建立字典 (字串會被拆成字元)
>>> a=dict.fromkeys(seq)
>>> a
{'2': None, '3': None, '1': None}               #字串被拆成字元當鍵

另外在測試串列時有提到內建函數 zip(), 此函數會將兩個 tuple 或 list 物件內的項目依序配對後以 tuple 的串列傳回一個 zip 物件 (是一個迭代物件), 可以很方便地用 dict() 轉成字典, 例如 :

>>> char=('A','B','C')        #用元組物件配對
>>> ascii=(41,42,43)
>>> a=zip(char,ascii)
>>> type(a)
<class 'zip'>
>>> dict(a)
{'A': 41, 'C': 43, 'B': 42}
>>> dict(a)                         #經 dict() 轉換後, zip 物件已被迭代完故為空
{}

注意, zip 物件經 dict() 轉換後會因為已經被迭代完而變成空. 迭代物件 zip 除了可用 dict() 轉成字典外, 也可以用 list() 或 tuple() 轉換成串列或元組. 下面使用 list 物件做配對 :

>>> char=['A','B','C']          #用串列物件配對
>>> ascii=[41,42,43]
>>> dict(zip(char,ascii))     #轉成字典
{'A': 41, 'C': 43, 'B': 42}
>>> list(zip(char,ascii))       #轉成串列
[('A', 41), ('B', 42), ('C', 43)]
>>> tuple(zip(char,ascii))    #轉成元組
(('A', 41), ('B', 42), ('C', 43))


2. 字典的基本操作 :

字典的基本操作如下 :
  1. 字典內容更改 
  2. 刪除項目 (用 del 指令)
  3. 查詢項目的鍵 (使用 in 與 not in 運算子)
字典物件建立後, 可用 [key] 運算子來存取其項目之值. 指派一個值給此項目時, 若字典裡已存在此鍵, 則既有的值就會被新指派的值替換; 若鍵是新的 (不存在), 則此鍵:值對會被新增到字典裡成為一個新的項目, 例如 :

>>> a={1:'a',2:'b',3:'c'}
>>> a
{3: 'c', 1: 'a', 2: 'b'}
>>> a[1]            #擷取字典項目  
'a'
>>> a[1]='A'      #變更項目的值
>>> a
{3: 'c', 1: 'A', 2: 'b'}
>>> a[4]='d'       #新增項目
>>> a
{4: 'd', 1: 'A', 2: 'b', 3: 'c'}

欲刪除字典中的一個項目, 與串列一樣, 可使用 del 指令並指定該項目之鍵, 如果鍵不存在會產生 KeyError 錯誤, 例如 :

>>> a={1:'a',2:'b',3:'c'}
>>> del a[2]       #刪除鍵=2 的項目
>>> a
{3: 'c', 1: 'a'}                      #已經無鍵=2 之項目了
>>> del a[2]       #欲刪除項目之鍵不存在產生錯誤
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
KeyError: 2

因此在刪除一個字典項目之前, 最好用 in 或 not in 指令查詢此鍵是否存在, 這兩個指令會傳回 True/False, 例如 :

>>> a={1:'a',2:'b',3:'c'}
>>> 2 in a        #key=2 之項目存在
True
>>> if 2 in a:   #若 key=2 存在就刪除此項目
...     del a[2]
...
...
...
>>> a
{3: 'c', 1: 'a'}                    #key=2 之項目已被刪除
>>> 4 in a         #key=4 之項目不存在
False
>>> if 4 in a:    #若 key=4 存在就刪除此項目
...     del a[4]
...
...
...
>>> a
{3: 'c', 1: 'a'}

3. 用內建函數操作字典 :

下列內建函數可以用在字典 :

 函數 說明
 len(obj) 傳回字典項目的個數
 min(obj) 傳回字典項目中鍵最小的元素
 max(obj) 傳回字典項目中鍵最大的元素
 sum(obj) 傳回字典項目所有之和 
 sorted(obj)  傳回鍵由小到大排列組成之串列

這些函數都是作用在鍵上, 適用於 key 為數值的字典, 例如 :

>>> a={1:'a',2:'b',3:'c'}
>>> len(a)        #傳回字典項目數目
3
>>> min(a)       #傳回最小的 key
1
>>> max(a)      #傳回最大的 key
3
>>> sum(a)      #傳回所有 key 的和
6
>>> sorted(a)   #依據 key 之值做升冪排序
[1, 2, 3]

如果用在鍵為字串的字典, 除了 sum() 無法運算會產生錯誤外, 都以字串的 ASCII 為比較之準則, 即數字先, 大寫字母次之, 小寫字母最後.

>>> a={'c':1,'a':2,'b':3}
>>> min(a)        #傳回 ASCII 最小的 key
'a'
>>> max(a)       #傳回 ASCII 最大的 key
'c'
>>> sum(a)       #字串無法求和
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unsupported types for __add__: 'int', 'str'
>>> sorted(a)    #依據 key 之 ASCII 值做升冪排序
['a', 'b', 'c']
>>> a={'aA':1,'AA':2,'aa':3,'Aa':4}
>>> sorted(a)
['AA', 'Aa', 'aA', 'aa']         #依據 key 之 ASCII 值做升冪排序


4. 字典物件的方法 :

 方法 說明 
 keys()  傳回字典的鍵 (傳回 dict_keys 迭代物件)
 values() 傳回字典的鍵 (傳回 dict_values 迭代物件)
 items() 傳回字典的項目 (傳回 dict_items 迭代物件)
 get(key [,default]) 傳回字典中鍵為 key 之值, 若鍵不存在傳回預設鍵 default 之值
 setdefault(key [,default]) 若鍵已存在傳回其值, 否則填入預設值
 pop(key) 從字典取出指定項目傳回, 並從字典中刪除該項目
 update(obj) 將字典 obj 內容加入目前字典中, 若鍵重複則以 obj 更新字典的值
 copy() 複製字典所有項目至另一字典複本, 並傳回其參考
 dict.fromkeys(seq [, default]) 以序列物件 seq 內容為鍵建立一個字典並傳回其參考 (類別方法)
 clear() 移除字典內的全部項目

注意上表中的 dict.fromkeys() 為字典型別的方法, 不是物件方法.

方法 keys() 會傳回一個 dict_keys() 可迭代物件, 裡面儲存字典所有的鍵. 傳回 dict_keys 物件而非像 Python 2.x 那樣直接傳回一個串列物件的原因是為了節省記憶體與時間. 不過我們可以用 list() 函數將 dict_keys 轉成 list 物件 :

>>> a={1:'a',2:'b',3:'c'}
>>> b=a.keys()                   #傳回字典全部的鍵
>>> b
dict_keys([3, 1, 2])
>>> type(b)
<class 'dict_view'>
>>> list(b)                           #轉成串列
[3, 1, 2]
>>> tuple(b)                        #轉成元組
(3, 1, 2)

方法 values() 會傳回儲存字典所有值的一個可迭代物件 dict_values(), 同樣也可以用 list() 將其轉成串列 :

>>> a={1:'a',2:'b',3:'c'}  
>>> c=a.values()                #傳回字典全部的值
>>> c
dict_values(['c', 'a', 'b'])    
>>> type(c)                      
<class 'dict_view'>
>>> list(c)                          #轉成串列
['c', 'a', 'b']
>>> tuple(c)                       #轉成元組
('c', 'a', 'b')

方法 items() 會傳回全部字典項目, 放在一個可迭代物件 dict_items() 裡, 可用 tuple() 或 list() 轉成元組或串列, 但每個項目是以 tuple 方式表示, 例如 :

>>> a={1:'a',2:'b',3:'c'}
>>> d=a.items()                  #傳回字典的所有項目
>>> d
dict_items([(3, 'c'), (1, 'a'), (2, 'b')])    #傳回 dict_items() 物件
>>> type(d)
<class 'dict_view'>
>>> list(d)                            #轉成串列
[(3, 'c'), (1, 'a'), (2, 'b')]
>>> tuple(a.items())             #轉成元組
((3, 'c'), (1, 'a'), (2, 'b'))

如果字典為空, 則上面 keys(), values(), 與 items()  三個方法也將傳回空的迭代物件, 用 tuple() 或 list() 轉換將得到一個空元組或空的串列.

如果要取得指定鍵所對應的值, 可用 get() 方法, 此方法可傳入兩個參數, 第一參數是要尋找的 key, 第二參數可有可無, 用來設定萬一傳入的鍵不存在時要傳回之值, 如果傳入的 key 不存在, 又沒有指定預設傳回值的話, get() 會傳回 None :

>>> a={1:'a',2:'b',3:'c'}
>>> a.get(1)                #鍵存在:傳回鍵對應之值
'a'
>>> a.get(2)
'b'
>>> a.get(3)
'c'
>>> a.get(4)                #鍵若不存在, 預設傳回 None
>>> print(a.get(4))
None
>>> a.get(4,'z')            #鍵若不存在, 傳回指定之預設值 'z'
'z'

方法 setdefault() 顧名思義是為字典不存在的 key 設定預設值, 實際上是為字典添加一個項目, 因此可以用來建立一個字典物件, 如上面建立字典物件的第三種方法所示. 此方法可以有兩個參數, 第一個必須的 key, 第二個為可有可無之預設值. 如果只傳入 key, 且此鍵已存在於字典內, 則此方法會傳回鍵所對應之值, 這時其作用就跟 get() 方法一樣; 若此鍵不存在, 就會為字典添加一個以此鍵為 key 的項目, 預設值為 None, 並傳回此預設值 None, 例如 :

>>> a={1:'a',2:'b',3:'c'}
>>> a.setdefault(2)                #只傳入 key, 則傳回此 key 對應之值
'b'
>>> a.setdefault(4)                #只傳入一個不存在的鍵, 傳回 None 並新增 key:None 項目
>>> print(a.setdefault(4))      #要用 print() 才看得到 None
None
>>> a                                  
{4: None, 1: 'a', 2: 'b', 3: 'c'}                  #新增了 key:None 項目

注意, setdefault() 只是用來設定預設值,  因此只有在鍵不存在時才有設值之作用 (因此可用來建立字典), 若鍵已存在則 setdefault() 無法更改其值, 例如 :

>>> a={1:'a',2:'b',3:'c'}
>>> a.setdefault(1,'A')    #鍵 1 已存在, 無法用傳入之第二參數更改其值
'a'                                                    #仍傳回鍵 1 原本之值 'a'
>>> a
{3: 'c', 1: 'a', 2: 'b'}

方法 pop() 是從字典中取出指定鍵之值, 同時將該項目從字典中移除. 若鍵不存在會產生 KeyError 錯誤, 例如 :

>>> a={1:'a',2:'b',3:'c'}
>>> a.pop(2)
'b'
>>> a
{3: 'c', 1: 'a'}                   #鍵=2項目 pop 後就被移除了
>>> a.pop(3)
'c'
>>> a
{1: 'a'}
>>> a.pop(3)   #pop 不存在的鍵會產生錯誤
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
KeyError: 3
>>> a.pop(1)
'a'
>>> a
{}                                     #項目全部被取出後變成空字典

方法 update() 可以用來合併兩個字典物件, 事實上就是將傳入的字典物件內容加入目前的字典中更新其內容, 如果傳入的字典內容中的 key 與目前字典重複, 就以傳入字典為主更新目前字典中該艦所對應之值, 這是 update() 名稱的原意. 注意, 傳入的字典本身不受 update() 方法的影響, 例如 :

>>> a={1:'a',2:'b',3:'c'}  
>>> a
{3: 'c', 1: 'a', 2: 'b'}
>>> b={1:'A',4:'E'}
>>> b
{4: 'E', 1: 'A'}
>>> a.update(b)      #把字典 b 合併到字典 a
>>> a
{4: 'E', 1: 'A', 2: 'b', 3: 'c'}      #以字典 b 為主更新字典 a
>>> b                      #字典 b 是被合併者, 不受 update() 影響
{4: 'E', 1: 'A'}

字典為 mutable 可變資料型態, 如果有兩個變數同時指向一個字典的參考, 則會有別名 (alias) 問題, 即更改其中一個內容, 另一個也會改變, 可以使用內建函數 id() 來查看物件的參考 (相當於記憶體位址, 有些實作例如 CPython 就是物件的記憶體位址), 如果兩個物件的 id 一樣, 表示是同一物件, 用 is 運算會回傳 True, 例如 :

>>> a={1:'a',2:'b',3:'c'}
>>> a
{3: 'c', 1: 'a', 2: 'b'}
>>> id(a)              #傳回字典 a 的 id (參考)
1073673600    
>>> b=a                #將字典 a 的參考設給物件 b, 兩者指向同一物件
>>> b
{3: 'c', 1: 'a', 2: 'b'}
>>> id(b)              #傳回字典 b 的 id (參考) : 跟字典 b 完全一樣
1073673600
>>> b is a              #檢查字典 a 與字典 b 是否指向同一物件 : 是
True
>>> b == a            #檢查字典 a 與字典 b 內容是否相同 : 是
True
>>> b[2]='A1'       #更改 b 的內容也會改到 a
>>> b
{3: 'c', 1: 'a', 2: 'A1'}
>>> a
{3: 'c', 1: 'a', 2: 'A1'}

而方法 copy() 可以複製一個字典的內容到複本, 並傳回此複本之參考, 這樣就可以得到一個內容完全一樣的新字典 (兩者參考的 id 不同), 彼此互不干擾, 例如 :

>>> a={1:'a',2:'b',3:'c'}
>>> a
{3: 'c', 1: 'a', 2: 'b'}
>>> id(a)              #傳回原字典的 id (參考)
1073673600    
>>> b=a.copy()    #複製字典內容到複本並傳回其參考
>>> b
{3: 'c', 1: 'a', 2: 'b'}
>>> id(b)              #傳回新字典的 id (參考)
1073673936                         #與字典 a 是不同的參考
>>> b is a             #檢查字典 a 與字典 b 是否指向同一物件 : 不是
False
>>> b == a           #檢查字典 a 與字典 b 內容是否相同 : 是
True
>>> b[1]='A'         #更改字典 b 不會影響字典 a
>>> b
{3: 'c', 1: 'A', 2: 'b'}
>>> a
{3: 'c', 1: 'a', 2: 'b'}

方法 dict.fromkeys() 為字典型別方法, 不是物件方法, 主要用來以傳入之序列物件為鍵建立一個字典, 如上面建立字典物件的第四種方法所示.

方法 clear() 會清除字典全部內容使其成為一個空字典, 例如 :

>>> a={1:'a',2:'b',3:'c'}
>>> a.clear()
>>> a
{}                       #變成空字典


二. 集合 set :

集合是一種內容可變, 元素無順序與不可重複的容器, 與字典一樣使用大括號 {} 將元素括起來. 集合的元素跟字典的鍵一樣不可重複, 因此集合也被視為等同於無值的字典. 集合的元素可以是任何不可變物件, 例如 int, tuple 與 str, 這些都是可雜湊 (hashable) 物件. 但要注意, tuple 容器中若含有可變元素, 則此 tuple 將無法計算雜湊值.

沒有元素的集合稱為空集合 (null set), 而集合的運算包括交集, 聯集, 差集, 以及互斥或. 注意, 集合本身雖是可變型別 (即元素可增減), 但其元素卻必須是不可變型別, 這樣的元素才能符合不可重複 (可雜湊) 的要求.

1. 建立集合 :

建立集合的第一種方法是使用大括號包住以逗號隔開的不可變物件字面值, 例如 :

>>> a={1,2,3,'a','b','c',True,False,3.14159}
>>> a
{False, 1, 2, 3, 'a', 'b', 'c', 3.14159}
>>> type(a)
<class 'set'>

集合的元素必須是不可變型別資料如 int, float, str, tuple 這四種, 其中 tuple 本身雖為不可變, 但若 tuple 內的元素含有 list, dict, set 等可變型別資料的話, 這個 tuple 就不算是不可變資料了, 這樣無法建立集合, 因其中的可變型別元素為不可雜湊 (inhashable), 例如 :

>>> a={0,1,(2,[3,4])}         #元組的元素是可變的串列, 無法計算雜湊值
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unsupported type for __hash__: 'list'
>>> a={0,1,{2:'b',3:'c'}}     #元組的元素是可變的字典, 無法計算雜湊值
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unsupported type for __hash__: 'dict'
>>> a={0,1,{2,3}}               #元組的元素是可變的集合, 無法計算雜湊值
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unsupported type for __hash__: 'set'

集合與字典都是使用大括號將元素或項目資料包起來, 但 Python 將 {} 優先給了字典, 即 {} 是空字典而非空集合, 建立一個空集合必須呼叫內建函數 set(), 例如 :

>>> a={}         #這是建立一個空字典, 不是空集合
>>> type(a)
<class 'dict'>    
>>> a=set()      #建立空集合必須使用 set()
>>> type(a)
<class 'set'>
>>> a
set()

建立集合的第二種方法是使用內建函數 set() 與集合物件的 add() 方法, 例如 :

>>> a=set()      #建立空集合
>>> a.add(1)    #為集合添加元素
>>> a.add(2)
>>> a.add(3)
>>> a.add('a')
>>> a.add('b')
>>> a.add('c')
>>> a
{'c', 1, 'a', 2, 3, 'b'}

可見集合元素的排列與加入之順序無關.

建立集合的第三種方法是將 list, tuple, str, dict 等容器物件傳給內建函數 set(), 其中串列與元組是直接轉換, 但會去除重複之元素; 而字串則被拆成字元的集合, 同樣也會去除重複之字元; 而字典則是將字典的 key 轉成集合傳回. 例如 :

>>> set([1,2,3,'a','b','c',2,'a'])      #將串列轉成集合 (會去除重複之元素)
{'c', 1, 'a', 2, 3, 'b'}
>>> set([1,2,3,'a','b','c',2,'a'])      #將元組轉成集合 (會去除重複之元素)
{'c', 1, 'a', 2, 3, 'b'}
>>> set('Hello')                          #將字串轉成集合  (會去除重複之字元)
{'e', 'H', 'l', 'o'}
>>> set({1:'a',2:'b',3:'c'})           #將字典轉成集合  (只轉 key)
{1, 2, 3}

內建函數 range() 產生的序列型別物件也可以傳給 set() 來產生集合, 例如 :

>>> set(range(5))
{0, 1, 2, 3, 4}


2. 集合的基本操作 :

集合的基本操作如下 :
  1. 檢查元素是否存在 (用 in 或 not in 指令)
  2. 交集  (使用 & 運算子)
  3. 聯集  (使用 | 運算子)
  4. 差集  (使用 - 運算子)
例如 :

>>> a={1,2,3,'a','b','c'}
>>> 2 in a
True
>>> 'c' in a
True
>>> 'd' in a
False
>>> 'b' not in a
False
>>> 'd' not in a
True

集合的數學運算包括交集, 聯集, 與差集, 例如 :

>>> a={1,2,3,'a','b'}
>>> b={2,3,'a','c'}
>>> a  & b
{'a', 2, 3}
>>> a | b
{'c', 1, 2, 3, 'a', 'b'}
>>> a - b
{'b', 1}

3. 用內建函數操作集合 : 

集合可用的內建函數如下 :

 函數 說明
 len(obj) 傳回物件元素個數
 min(obj) 傳回物件中數值最小的元素 
 max(obj) 傳回物件中數值最大的元素
 sum(obj) 傳回物件所有元素之和
 sorted(obj)  傳回升冪排列之串列

若元素全部是數值, 則每一個函數均可運算 :

>>> a={5,4,3,2,1}
>>> len(a)
5
>>> min(a)
1
>>> max(a)
5
>>> sum(a)
15
>>> sorted(a)  
[1, 2, 3, 4, 5]

若元素都是字串, 則除了 sum() 無法運算外, 其他函數都是從左至右逐一比對 ASCII 碼, 例如 :

>>> a={'aA','AA','aa','Aa'}
>>> len(a)
4
>>> min(a)    #'AA' 的 ASCII 最小
'AA'
>>> max(a)   #'aa' 的 ASCII 最小
'aa'
>>> sum(a)   #字串無法加總
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unsupported types for __add__: 'int', 'str'
>>> sorted(a)     #按照 ASCII 碼升冪排序
['AA', 'Aa', 'aA', 'aa']

但是如果元素是數值與字串混雜, 則只有 len() 可執行, 其他函數均無法運算 :

>>> a={1,2,3,'a','b','c'}   #元素有數值也有字串
>>> len(a)     #只有 len() 能執行
6
>>> min(a)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unsupported types for __lt__: 'int', 'str'
>>> max(a)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unsupported types for __gt__: 'int', 'str'
>>> sum(a)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unsupported types for __add__: 'int', 'str'
>>> sorted(a)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unsupported types for __lt__: 'int', 'str'

4. 元素物件的方法 :

 方法 說明 
 add(obj) 新增元素 obj 至集合中
 remove(obj) 移除元素 obj, 移除元素 obj, 若不存在會產生錯誤
 discard(obj) 移除元素 obj, 若不存在則 pass, 不會產生錯誤
 pop() 從集合左方取出一個元素, 若不存在會產生錯誤
 clear()  刪除集合內的全部元素 (成為空集合)
 issubset(obj) 檢查集合 obj 是否為子集合 (傳回 True/False), 等同於 <= 運算子
 issuperset(obj) 檢查集合 obj 是否為超集合 (傳回 True/False), 等同於 >= 運算子
 union(obj) 傳回與集合 obj 之聯集, 等同於 | 運算子
 intersection(obj) 傳回與集合 obj 之交集, 等同於 & 運算子
 difference(obj) 傳回與集合 obj 之差集, 等同於 - 運算子

由於集合為可變型別, 因此可以用 add(), remove(), discard(), pop(), remove(), 與 discard() 方法更改集合的內容, 其中 remove() 若刪除不存在的元素會產生 KeyError 錯誤, 而 discard() 則不會. 而 pop() 方法會從集合左方取出指定元素傳回, 例如 :

>>> a={1,2,3}
>>> a.add('a')        #新增元素
>>> a.add('b')        #新增元素
>>> a.add('c')        #新增元素
>>> a
{'c', 1, 'a', 2, 3, 'b'}
>>> a.remove('c')          #刪除元素
>>> a
{1, 'a', 2, 3, 'b'}
>>> a.remove('c')          #刪除不存在的元素 (產生 KeyError 錯誤)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
KeyError:
>>> a.discard('c')           #刪除不存在的元素 (不會產生錯誤)
>>> a
{1, 'a', 2, 3, 'b'}
>>> a.pop()                    #取出並刪除集合左方元素後傳回
1
>>> a.pop()
'a'
>>> a                              #取出 1, 'a' 後剩下 2,3,'b'
{2, 3, 'b'}
>>> a.pop()
2
>>> a                              #取出 1, 'a', 2 後剩下 3,'b'
{3, 'b'}
>>> a.clear()                   #移除集合中的全部元素
>>> a
set()

另外, union(), intersection(), 與 difference() 三個方法則是用來做集合之聯集, 交集, 以及差集運算, 分別等同於 |, &, 以及 - 運算子的功能, 例如 :

>>> a={1,2,3,'a','b'}
>>> b={2,3,'a','c'}
>>> a.union(b)             #聯集
{'c', 1, 2, 3, 'a', 'b'}
>>> a | b                       #聯集
{'c', 1, 2, 3, 'a', 'b'}
>>> a.intersection(b)    #交集
{'a', 2, 3}
>>> a & b                     #交集
{'a', 2, 3}
>>> a.difference(b)       #差集
{'b', 1}
>>> a - b                       #交集
{'b', 1}

終於完成字典與集合測試! 這部分 MicroPython 都有實作.

沒有留言 :