Python中的不可變集合

songofhawk發表於2022-01-23

Python內建了一個不可變(immutable)集合物件:frozenset,顧名思義,這種集合物件,一旦建立了就不能修改。

概念

下面的程式碼演示了可變和不可變集合使用上的差別:

l = [1, 2, 3, 4]
new_set = set(l)
new_set.remove(1)
print (new_set)

fset = frozenset(l)
fset.remove(1)

如果執行這段程式碼,它的輸出會是:

{2, 3, 4}

Traceback (most recent call last):
  File "main.py", line 9, in <module>
    fset.remove(1)
AttributeError: 'frozenset' object has no attribute 'remove'

可以看到frozenset例項根本沒有remove方法,當然它也沒有append方法,不能用index給元素賦值。

作用

除了不可修改以外,frozenset所有的用法都和普通set一樣。

這樣一個不可變的集合,有什麼用呢?對效能應該沒有什麼幫助,曾經有文章指出它比普通的set還稍慢一點,主要價值還是體現在可讀性和安全性上:

  • 顯式地宣告瞭該集合是不可更改的,給使用者明確提示,一旦誤用了,執行時也會友好報錯;
  • 如果想把這個集合,放到另一個set裡面,或者作為dict的key,那它就必須是不可更改的

這第二點,主要是因為dict的key要求是唯一的(或者set內的元素也是唯一的),要是傳一個物件,python會嘗試呼叫它的hash方法生個雜湊串,作為判斷是否唯一的依據,而如果這個物件居然是可變的,它的內容就會影響雜湊串的值。所以在python裡,可變物件根本就沒有內建hash方法,避免它被當做key。

比如下面這段程式碼:

fset = frozenset({4, 5})
s = {1, 2, fset}
print (s)

s = {1, 2, {4, 5}}

首先定義了一個frozenset,把它插入一個普通set;最後一句試圖在普通set裡面,初始化另外一個普通set。輸出結果如下:

{frozenset({4, 5}), 1, 2}

Traceback (most recent call last):
  File "main.py", line 27, in <module>
    s = {1, 2, {4, 5}}
TypeError: unhashable type: 'set'

可見普通set是不能作為另外一個set的元素的,但frozenset就可以。‘

為什麼Java沒有這個限制

Java程式設計師可能會覺得奇怪,在hashset裡放一個可變物件,不是很正常麼?憑什麼不行啊,我們Java經常這樣寫:

HashSet<ArrayList<String>> masterCollection = new HashSet<ArrayList<String>>();
ArrayList<String> a = new ArrayList<String>();
masterCollection.add(a);
a.add("Hello, World");
for(ArrayList<String> list : masterCollection) {
    // do something to list
}

完全不會報錯麼。

So,難道Java的實現機制和Python不一樣?

答案是並沒有,Java只是“容忍”了可能發生的錯誤——這個帖子解釋得很清楚。簡單來說:

Java和Python都是用一個物件的hash值作為key的,相比Python更重視資料一致性,Java更看中靈活性,所以如果我們真的在HashSet裡放了一個可變物件,並且事後改變了它的值,那麼就會發現,HashSet集合,已經檢測不到它的存在了!

mutable set as key in java.png

相關文章