2.10. 集合

集合(set)是由唯一項目組成的無序集合。加入一個已經存在的值不會有任何效果;迭代時每個值恰好產生一次。當成員測試與去除重複很重要、而順序無關緊要時,集合正是合適的工具。

2.10.1. 建立集合

對非空集合使用大括號,對空集合則使用 set()

colours = {"red", "green", "blue"}
empty = set()

大括號看起來像 dict 字面值;單獨的 {} 是一個空字典,而不是一個空集合——這是 Python 歷史上的意外之一。空集合的情況請使用 set()

set() 也能從任何可迭代物件建立集合,這是從序列中去除重複項的標準做法:

nums = [1, 2, 2, 3, 1, 4]
unique = set(nums)
print(unique)

輸出::

{1, 2, 3, 4}

列印順序可能會有所不同——集合並不保證以任何特定順序迭代。

2.10.2. 集合與字典的比較

集合與字典都在雜湊表中儲存唯一的項目。差別在於每個項目隨身攜帶了什麼:

  • dict 儲存鍵值配對。查找一個鍵會回傳它的值。

  • set 只儲存項目本身。查找一個項目會告訴你它是否在其中。

兩者之間的選擇取決於每個項目旁的值是否具有任何意義:

  • 當沒有任何值需要伴隨每個項目時,選用集合——你只關心項目是否存在,或是你正在用聯集/交集合併多組唯一項目。

  • 當每個項目都與某個查找時要取回的資料配對時,選用字典——例如組態對應表、快取,或以名稱為鍵的計數器。

這兩種型別共用許多表面語法,這正是大部分混淆的來源。它們的差異整理在一個區塊中:

set

dict

儲存

唯一項目

唯一的鍵,每個都帶有一個值

有內容的字面值

{1, 2, 3}

{"a": 1, "b": 2}

空字面值

set()

{}

成員測試

x in s

k in d(僅限鍵)

取得一個值

不適用

d[k]

新增一個項目

s.add(x)

d[k] = v

迭代

產生項目

產生鍵(配對請使用 d.items()

有內容字面值與空字面值之間的不對稱,是值得特別指出的陷阱:

  • 含有項目的大括號—— {1, 2, 3} ——是集合字面值;含有鍵值配對的大括號—— {"a": 1} ——是字典字面值。解析器是依據裡面的內容來區分它們。

  • 裡面什麼都沒有的大括號—— {} ——是一個空字典,而不是空集合。字典先出現;空字面值歸屬於它們。空集合根本沒有大括號字面值,必須寫成 set()

當一個字典只會讀取它的鍵時,常見的做法是改用集合——這能讓意圖一目了然,並把未使用的值從記憶體中省去。

2.10.3. 新增與移除

s = {1, 2, 3}
s.add(4)
s.discard(99)            # silent: 99 not in s
s.remove(2)
print(s)

輸出::

{1, 3, 4}

2.10.4. 成員測試

in 運算子會測試成員是否存在。在集合上,無論大小,它大致都是常數時間——這正是當你只需要詢問「這個值在不在裡面」時,選用集合而非 list 的主要原因:

if "red" in colours:
    print("colour is allowed")

內容相同的 list 每次都會從頭開始掃描,對十個項目來說沒問題,但對一萬個項目就會很慢。

2.10.5. 集合運算

兩個集合可以用常見的數學運算來組合。每種運算都有運算子形式與方法形式:

  • a | ba.union(b) ——位於任一集合中的所有項目。

  • a & ba.intersection(b) ——只取同時出現在兩者中的項目。

  • a - ba.difference(b) ——位於 a 中但不在 b 中的項目。

  • a ^ ba.symmetric_difference(b) ——位於其中一者但不同時位於兩者中的項目。

a = {1, 2, 3, 4}
b = {3, 4, 5, 6}
print(a | b)
print(a & b)
print(a - b)
print(a ^ b)

輸出::

{1, 2, 3, 4, 5, 6}
{3, 4}
{1, 2}
{1, 2, 5, 6}

運算子形式是唯讀的;方法形式可接受右側的任何可迭代物件,而不僅限於另一個集合(a.union([5, 6]))。請挑選在當下情境中讀起來較順暢的形式。

2.10.6. 什麼能放進集合

集合的元素必須是可雜湊的(hashable)——與 dict 鍵的限制相同。intfloatstrboolbytes,以及 tuple(當其內容本身都是可雜湊時)都可以。listdict 則不行;嘗試加入它們會引發 TypeError

2.10.7. frozenset

一般的 set 是可變的:每次呼叫 addremovediscard 都會就地改變該物件。這種可變性使它無法成為可雜湊的,因此集合不能用作 dict 的鍵,也不能作為另一個集合的成員。

frozenset 是它的不可變對應版本。它擁有與 set 相同的查找與運算子(in|&-^),但沒有 addremove,也沒有任何會變更內容的方法。由於它的內容永遠無法改變,frozenset 的雜湊值是明確定義的——所以它可雜湊的:

primary = frozenset({"red", "green", "blue"})
secondary = frozenset({"yellow", "purple", "orange"})

palettes = {
    primary: "RGB",
    secondary: "mixed",
}

print(palettes[primary])

輸出::

RGB

可從任何可迭代物件建構 frozenset—— frozenset() 用於空的情況,frozenset(some_set) 則用於取得既有集合的不可變快照:

snapshot = frozenset(s)         # immutable copy of s
s.add("new")                    # snapshot does not change

選用它有兩個常見的理由:

  • 用作字典的鍵或集合的成員。 在任何單一值無法表達你所需內容的地方,由多個值組成的 frozenset 就能勝任——例如「此驅動程式所支援的功能集合」、「此設定檔所使用的接腳集合」。

  • 鎖定一個常數。 模組層級的 frozenset(存放允許的名稱)無法被呼叫端意外變更;一般的 set 則可以。對於任何在建構後就應為唯讀的內容,請優先使用 frozenset