问题描述
在下面,当我尝试对列表进行哈希处理时,它给了我一个错误,但可以使用元组。 猜测它与不变性有关。 有人可以详细解释一下吗?
列表
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}
1楼
字典和其他对象使用来非常快速地存储和检索项目。
这一切的机制都发生在“幕后”——你作为程序员不需要做任何事情,Python 在内部处理这一切。
基本思想是,当您使用{key: value}
创建字典时,Python 需要能够散列您用于key
任何内容,以便它可以快速存储和查找值。
不可变对象或无法更改的对象是可散列的。
它们有一个永远不会改变的唯一值,因此 python 可以“散列”该值并使用它来有效地查找字典值。
属于这一类的对象包括字符串、元组、整数等。
你可能会想,“但我可以改变一个字符串!我只是去mystr = mystr + 'foo'
,”但实际上这样做是创建一个新的字符串实例并将它分配给mystr
。
它不会修改现有实例。
不可变对象永远不会改变,因此您始终可以确保为不可变对象生成哈希时,通过哈希查找对象将始终返回与您开始时相同的对象,而不是修改后的版本。
你可以自己试试: hash("mystring")
, hash(('foo', 'bar'))
, hash(1)
可变对象或可以修改的对象不可散列。
可以就地修改列表: mylist.append('bar')
或mylist.pop(0)
。
您不能安全地散列可变对象,因为您不能保证该对象自上次看到它以来没有改变。
您会发现list
、 set
和其他可变类型没有__hash__()
方法。
因此,您不能使用可变对象作为字典键:
>>> hash([1,2,3])
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: unhashable type: 'list'
的回答提供了一个很好的例子,说明使用可变对象作为字典键时出现的意外行为
2楼
以下是为什么允许可变类型作为键可能不是一个好主意的示例。 这种行为在某些情况下可能很有用(例如,使用对象的状态作为键而不是对象本身),但它也可能导致令人惊讶的结果或错误。
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'
3楼
散列集计算对象的散列,并基于该散列将对象存储在结构中以进行快速查找。 因此,根据合同,一旦将对象添加到字典中,就不允许更改哈希值。 大多数好的散列函数将取决于元素的数量和元素本身。
元组是不可变的,所以在构造之后,值不能改变,因此哈希也不能改变(或者至少一个好的实现不应该让哈希改变)。
另一方面,列表是可变的:稍后可以添加/删除/更改元素。 因此,散列可能会违反合约而改变。
因此,所有不能保证在添加对象后哈希函数保持稳定的对象都违反了合约,因此不是好的候选对象。 因为对于lookup ,字典会先计算key的hash,然后确定正确的bucket。 如果同时更改了键,这可能会导致误报:对象在字典中,但无法再检索,因为散列不同,因此将搜索与最初添加对象的桶不同的桶.
4楼
我想添加以下方面,因为它尚未包含在其他答案中。
使可变对象可散列并没有错,它不是明确的,这就是为什么它需要由程序员自己(而不是由编程语言)一致地定义和实现。
请注意,您可以为任何自定义类实现__hash__
方法,该方法允许将其实例存储在需要可哈希类型的上下文中(例如 dict 键或集合)。
哈希值通常用于决定两个对象是否代表同一事物。
因此,请考虑以下示例。
您有一个包含两项的列表: l = [1, 2]
。
现在您向列表中添加一个项目: l.append(3)
。
现在您必须回答以下问题:它仍然是同一件事吗?
两者 - 是和否 - 都是有效的答案。
“是”,它仍然是相同的列表,“否”,它不再是相同的内容。
所以这个问题的答案取决于你作为程序员,所以你可以为你的可变类型手动实现散列方法。
5楼
基于
如果一个对象的哈希值在其生命周期内永远不会改变(它需要一个 __hash__() 方法),并且可以与其他对象进行比较(它需要一个 __eq__() 方法),那么它就是可哈希的。 比较相等的可散列对象必须具有相同的散列值。
Python 的所有不可变内置对象都是可散列的; 可变容器(例如列表或字典)不是。
6楼
因为列表是可变的,而元组则不是。 例如,当您将值的哈希存储在字典中时,如果对象发生更改,则存储的哈希值将无法找到,因此它将保持不变。 下次查找对象时,字典将尝试通过不再相关的旧哈希值来查找它。
为了防止这种情况,python 不允许您拥有可变项目。