繁体   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