很多python初學者經常會有這樣的疑問,為什麼python有tuple(元組)和list(列表)兩種型別?為什麼tuple可以作為字典的key,list不可以?要理解這個問題,首先要明白python的字典工作原理。
在python中,字典也就是乙個個的「對映」,將key對映到value:
# 對乙個特定的key可以得到乙個value
value = d[key]
為了實現這個功能,python必須能夠做到,給出乙個key,找到哪乙個value與這個key對應。先來考慮一種比較簡單的實現,將所有的key-value鍵值對存放到乙個list中,每當需要的時候,就去遍歷這個list,用key去和鍵值對的key匹配,如果相等,就拿到value。但是這種實現在資料量很大的時候就變得很低效。它的演算法複雜度是o(n),n是存放鍵值對的數量。(關於hash表具體的工作原理,可以參考我的這篇文章。
為此,python使用了hash(雜湊)的方法來實現,要求每乙個存放到字典中的物件都要實現hash函式,這個函式可以產生乙個int值,叫做hash value(雜湊值),通過這個int值,就可以快速確定物件在字典中的位置。然而,由於hash碰撞的存在,可能存在兩個物件的hash值是相同的,所以查詢字典的過程中,要比較hash值,還要比較value的值。
這個查詢的大致過程如下:
def lookup(d, key):
'''字典的查詢過程概括為下面3步:
1. 通過hash函式將key計算為雜湊值.
2. 通過hash值確定乙個位置,這個位置是乙個存放著
可能存在衝突的元素的陣列(很多地方叫做「桶」,bucket),
每乙個元素都是乙個鍵值對,理想情況下,這個陣列裡只有1個元素.
3. 遍歷這個陣列,找到目標key,返回對應的value.
h = hash(key) # step 1
cl = d.data[h] # step 2
for pair in cl: # step 3
if key == pair[0]:
return pair[1]
else:
raise keyerror, "key %s not found." % key
要使這個查詢過程正常工作,hash函式必須滿足條件:如果兩個key產生了不同的hash value,那麼這兩個key物件是不相等的。即
for all i1, i2, if hash(i1) != hash(i2), then i1 != i2
否則的話,hash value不同,物件卻相同,那麼相同的物件產生不同的hash value,查詢的時候就會進錯桶(step 2),在錯誤的桶裡永遠也找不到你要找的value。
另外,要讓字典保持高查詢效率,還要保證:當兩個key產生相同的hash value,那麼他們是相等的。
for all i1, i2, if hash(i1) == hash(i2), then i1 == i2
這樣做的目的是,盡量滿足每個hash桶只有乙個元素。為什麼要這樣呢? 考慮下面這個hash函式。
def hash(obj):
return 1
這個hash函式是滿足上面我們談的第乙個條件的:如果兩個key的hash value不同,那麼兩個key物件不相同。因為所有的物件產生的hash value都是1,所以不存在能產生不同hash value的key,也就不存在不滿足的情況。但是這樣做的壞處是,因為所有的hash value都相同,所以就把所有的物件分到了同乙個地方。查詢的時候,進行到第三步,遍歷的效率就變成了o(n).
hash函式應該保證所有的元素平均的分配到每乙個桶中,理想的情況是,每乙個位置只有乙個元素。
以上兩個原則,第乙個保證了你能從字典中拿到要找的元素,第二個保證了查詢效率。
經過上面的討論,我們應該明白python為什麼對字典的key有這樣的要求了:
要作為字典的key,物件必須要支援hash函式(即__hash__),相等比較(__eq__或__cmp__),並且滿足上面我們討論過的條件。
至於這個問題,最直接的答案就是:list沒有支援__hash__方法,那麼為什麼呢?
對於list的hash函式,我們可能有下面兩種實現的方式:
第一種,基於id。這滿足條件——「如果hash值不同,那麼他們的id當然不同」。但考慮到list一般是作為容器,基於id來hash可能會導致下面兩種情況:
第二種,基於內容。tuple就是這樣做的,但是要注意一點,tuple是不可以修改的,但list是可以修改的。當list修改之後,你就永遠別想再從字典中拿回來了。見下面的**。
>>> l = [1, 2]
>>> d = {}
>>> d[l] = 42
>>> d[l] # 原來的hash值是基於[1, 2]hash的,
# 現在是基於[1, 2, 3],所以找不到
traceback (most recent call last):
file "", line 1, in ?
keyerror: [1, 2, 3]
>>> d[[1, 2]] # 基於hash [1, 2]
# 但是遍歷的時候找不到key相等的鍵值對
#(因為字典裡的key變成了[1, 2, 3]
traceback (most recent call last):
file "", line 1, in ?
keyerror: [1, 2]
鑑於兩種實現的方式都存在一定的***,所以python規定:
內建的list不能作為字典的key.
但tuple是不可變,所以tuple可以作為字典的key。
(2023年1月2日更新,上面我說tuple不可變可以作為字典的key,這句話並不是完全正確的。tuple只是相對不可改變的,如果tuple中有元素是可變物件,那麼雖然tuple不可改變,那麼其中元素所指向的物件是可變的,所以同樣會出現上面「list不能作為字典的key」這個問題,即含有可變物件的tuple也不能作為字典的key,舉個例子就很好懂了。)
in [11]: li = [1,2,]
in [12]: d = dict()
in [13]: t2 = (1,2,)
in [14]: t3 = (1,2,li,)
in [15]: d[li] = 1
typeerror traceback (most recent call last)
in ()
----> 1 d[li] = 1
typeerror: unhashable type: 'list'
in [16]: d[t2] = 2
in [17]: d[t3] = 3
typeerror traceback (most recent call last)
in ()
----> 1 d[t3] = 3
typeerror: unhashable type: 'list'
使用者自定義的型別就可以作為key了,預設的hash(object)是 id(object), 預設的cmp(object1, object2)是cmp(id(object1), id(object2)),同樣是可以修改的物件,為什麼這裡就沒有上面說的問題呢?
一般來說,在對映中比較常見的需求是用乙個object替換掉原來的,所以id比內容更重要,就可以基於id來hash
如果內容重要的話,自定義的型別可以通過覆蓋__hash__函式和__cmp__函式或__eq__函式來實現
值得注意的是:將物件和乙個value關聯起來,更好的做法是將value設定為物件的乙個屬性。
list為什麼不能作為字典的key
list為什麼不能作為key 至於這個問題,最直接的答案就是 list沒有支援 hash 方法,那麼為什麼呢?對於list的hash函式,我們可能有下面兩種實現的方式 第一種,基於id。這滿足條件,如果hash值不同,那麼他們的id當然不同 但考慮到list一般是作為容器,基於id來hash可能會導...
Python 為什麼比 list 快?
在日常使用 python 時,我們經常需要建立乙個列表,相信大家都很熟練了吧?方法一 使用成對的方括號語法 list a 方法二 使用內建的 list list b list 上面的兩種寫法,你經常使用哪乙個呢?是否思考過它們的區別呢?對於第乙個問題,使用timeit模組的 timeit 函式就能簡...
為什麼 比list()更快?
我最近比較了和list 的處理速度,並且驚訝地發現執行速度比list 快三倍以上。我跑了相同的測試與 和dict 結果幾乎相同 和 兩個花了大約0.128sec 百萬次,而list 和dict 大約花費每個0.428sec 萬次。後來我查了查原因,得到的結論如下 list 需要全域性查詢和函式呼叫,...