簡體   English   中英

在 Python 中,為什么元組可以散列而不是列表?

[英]In Python, why is a tuple hashable but not a list?

在下面,當我嘗試對列表進行哈希處理時,它給了我一個錯誤,但可以使用元組。 猜測它與不變性有關。 有人可以詳細解釋一下嗎?

列表

 x = [1,2,3]
 y = {x: 9}
  Traceback (most recent call last):
   File "<stdin>", line 1, in <module>
 TypeError: unhashable type: 'list'

元組

z = (5,6)
y = {z: 89}
print(y)
{(5, 6): 89}

字典和其他對象使用哈希來非常快速地存儲和檢索項目。 這一切的機制都發生在“幕后”——你作為程序員不需要做任何事情,Python 在內部處理這一切。 基本思想是,當您使用{key: value}創建字典時,Python 需要能夠散列您用於key任何內容,以便它可以快速存儲和查找值。

不可變對象或無法更改的對象是可散列的。 它們有一個永遠不會改變的唯一值,因此 python 可以“散列”該值並使用它來有效地查找字典值。 屬於這一類的對象包括字符串、元組、整數等。 你可能會想,“但我可以改變一個字符串!我只是去mystr = mystr + 'foo' ,”但實際上這樣做是創建一個新的字符串實例並將它分配給mystr 它不會修改現有實例。 不可變對象永遠不會改變,因此您始終可以確保為不可變對象生成哈希時,通過哈希查找對象將始終返回與您開始時相同的對象,而不是修改后的版本。

你可以自己試試: hash("mystring") , hash(('foo', 'bar')) , hash(1)

可變對象或可以修改的對象不可散列。 可以就地修改列表: mylist.append('bar')mylist.pop(0) 您不能安全地散列可變對象,因為您不能保證該對象自上次看到它以來沒有改變。 您會發現listset和其他可變類型沒有__hash__()方法。 因此,您不能使用可變對象作為字典鍵:

>>> hash([1,2,3])
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unhashable type: 'list'

Eric Duminil的回答提供了一個很好的例子,說明使用可變對象作為字典鍵時出現的意外行為

以下是為什么允許可變類型作為鍵可能不是一個好主意的示例。 這種行為在某些情況下可能很有用(例如,使用對象的狀態作為鍵而不是對象本身),但它也可能導致令人驚訝的結果或錯誤。

Python

通過在list的子類上定義__hash__ ,可以使用數字列表作為鍵:

class MyList(list):
    def __hash__(self):
        return sum(self)

my_list = MyList([1, 2, 3])

my_dict = {my_list: 'a'}

print(my_dict.get(my_list))
# a

my_list[2] = 4  # __hash__() becomes 7
print(next(iter(my_dict)))
# [1, 2, 4]
print(my_dict.get(my_list))
# None
print(my_dict.get(MyList([1,2,3])))
# None

my_list[0] = 0  # __hash_() is 6 again, but for different elements
print(next(iter(my_dict)))
# [0, 2, 4]
print(my_dict.get(my_list))
# 'a'

紅寶石

在 Ruby 中,允許使用列表作為鍵。 Ruby 列表稱為Array而 dict 稱為Hash ,但語法與 Python 的 非常相似:

my_list = [1]
my_hash = { my_list => 'a'}
puts my_hash[my_list]
#=> 'a'

但是如果修改了這個列表,dict 就再也找不到對應的值了,即使鍵還在 dict 中:

my_list << 2

puts my_list
#=> [1,2]

puts my_hash.keys.first
#=> [1,2]

puts my_hash[my_list]
#=> nil

可以強制 dict 再次計算密鑰哈希:

my_hash.rehash
puts my_hash[my_list]
#=> 'a'

散列集計算對象的散列,並基於該散列將對象存儲在結構中以進行快速查找。 因此,根據合同,一旦將對象添加到字典中,就不允許更改哈希值 大多數好的散列函數將取決於元素的數量和元素本身。

元組是不可變的,所以在構造之后,值不能改變,因此哈希也不能改變(或者至少一個好的實現不應該讓哈希改變)。

另一方面,列表是可變的:稍后可以添加/刪除/更改元素。 因此,散列可能會違反合約而改變。

因此,所有不能保證在添加對象后哈希函數保持穩定的對象都違反了合約,因此不是好的候選對象。 因為對於lookup ,字典會先計算key的hash,然后確定正確的bucket。 如果同時更改了鍵,這可能會導致誤報:對象在字典中,但無法再檢索,因為散列不同,因此將搜索與最初添加對象的桶不同的桶.

我想添加以下方面,因為它尚未包含在其他答案中。

使可變對象可散列並沒有錯,它不是明確的,這就是為什么它需要由程序員自己(而不是由編程語言)一致地定義和實現。

請注意,您可以為任何自定義類實現__hash__方法,該方法允許將其實例存儲在需要可哈希類型的上下文中(例如 dict 鍵或集合)。

哈希值通常用於決定兩個對象是否代表同一事物。 因此,請考慮以下示例。 您有一個包含兩項的列表: l = [1, 2] 現在您向列表中添加一個項目: l.append(3) 現在您必須回答以下問題:它仍然是同一件事嗎? 兩者 - 是和否 - 都是有效的答案。 “是”,它仍然是相同的列表,“否”,它不再是相同的內容。

所以這個問題的答案取決於你作為程序員,所以你可以為你的可變類型手動實現散列方法。

基於Python 詞匯表

如果一個對象的哈希值在其生命周期內永遠不會改變(它需要一個 __hash__() 方法),並且可以與其他對象進行比較(它需要一個 __eq__() 方法),那么它就是可哈希的。 比較相等的可散列對象必須具有相同的散列值。

Python 的所有不可變內置對象都是可散列的; 可變容器(例如列表或字典)不是。

因為列表是可變的,而元組則不是。 例如,當您將值的哈希存儲在字典中時,如果對象發生更改,則存儲的哈希值將無法找到,因此它將保持不變。 下次查找對象時,字典將嘗試通過不再相關的舊哈希值來查找它。

為了防止這種情況,python 不允許您擁有可變項目。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM