【问题标题】:In Python, why is a tuple hashable but not a list?在 Python 中,为什么元组可以散列而不是列表?
【发布时间】:2020-03-30 13:46:23
【问题描述】:

下面当我尝试对列表进行哈希处理时,它给了我一个错误,但适用于一个元组。猜想它与不变性有关。有人可以详细解释一下吗?

列表

 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}

【问题讨论】:

  • 是的,它是关于一个可变的列表。如果您将哈希值存储在某处(例如,作为dict 的一部分)然后您修改列表,那么它不再是有效的字典键。这并不意味着可变对象具有散列值是没有意义的,而是这样的散列值没有明确的解释,因此由程序员在整个应用程序中一致地实现这一点。
  • 元组并不总是可散列的。 (1, 2, [3]) 不可散列,因为它的第三个元素不可散列。
  • @P.Camilleri 暗示元组上的 hash 是递归的。这很有趣,我以前不知道。
  • @MarkRansom AFAIK,元组的哈希(基本上)只是通过首先对每个元素进行哈希计算,然后对这些结果执行 。这让你的元组是可散列的,只要每个内容都可以单独散列。

标签: python


【解决方案1】:

字典和其他对象使用hashes 非常快速地存储和检索项目。这一切的机制都发生在“幕后”——作为程序员的你不需要做任何事情,Python 会在内部处理这一切。基本思想是,当您使用 {key: value} 创建字典时,Python 需要能够散列您用于 key 的任何内容,以便它可以快速存储和查找值。

不可变对象或无法更改的对象是可散列的。它们有一个永远不会改变的唯一值,因此 python 可以“散列”该值并使用它来有效地查找字典值。属于这一类的对象包括字符串、元组、整数等。你可能会想,“但我可以改变一个字符串!我只是去mystr = mystr + 'foo'”,但实际上它的作用是创建一个new 字符串实例并将它分配给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 的回答提供了一个很好的例子,说明使用可变对象作为字典键会导致意外行为

【讨论】:

  • 使可变对象可散列并没有错,您只需要在应用程序中保持一致和准确即可。您可以为任何自定义类实现__hash__ 方法,使其可散列。您甚至可以从list 继承,然后通过实现__hash__ 使其可散列。可变对象的哈希值是什么并不是明确的,这就是 Python 将其留给程序员来决定和定义的原因。
  • @a_guest 您让自己对意外行为保持开放 - 如果您的哈希方法不依赖于对象的内容,则两个对象可能共享相同的哈希。另一方面,如果您的对象是可变的,您可能会遇到设置{obj: value} 但后来无法检索mydict.get(obj) 的情况,因为对象(因此哈希)已更改。这在 python 中是可能的,我相信你可以找到一个甚至可能有意义的场景......但作为一般的经验法则, 使可变的事情有问题对象可散列。
  • 不,你没有明白这一点。首先,哈希值的“冲突”总是会发生(对于不可变的对象也是如此),这就是为什么哈希表使用链表来存储冲突的对象。其次(“反面”),如果可变对象的哈希在修改时发生变化,则取决于程序员。这两种情况都有意义(无论是否更改哈希),但它需要在整个应用程序中保持一致。因为 Python 是一种编程语言,他们决定把它留给程序员。
  • @a_guest 问题是关于本机数据类型的,所以它不是“由程序员决定的”。您是对的,完全有可能构造一个既可变又安全可清洗的对象,但这一点与这个问题没有任何关系——作为一般做法,这听起来像是一个令人难以置信的深奥用例。
【解决方案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,字典称为Hash,但语法与 Python 非常相似:

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

但是如果这个列表被修改了,dict就再也找不到对应的值了,即使key还在dict里面:

my_list << 2

puts my_list
#=> [1,2]

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

puts my_hash[my_list]
#=> nil

可以强制dict再次计算key hash:

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

【讨论】:

  • 赞成为什么使用可变对象作为键不是一个好主意的实际示例。
  • 使用可变对象作为 dict 键是一个坏主意,这是一种误解。这实际上取决于您的应用程序中的定义。您需要唯一保证的是两个比较相等的对象具有相同的哈希值。或者反过来:具有不同哈希值的两个对象比较不相等。另请注意,list 不是唯一存在的可变类型。当涉及到自定义类型时,可散列的可变对象更有意义。所以这个例子在某种意义上失败了,它似乎将“list”设置为等于“mutable type”。
  • @a_guest 感谢您的评论。我写道,这可能是个坏主意。
  • 可能值得补充的是,它可能是一个好主意,只要清楚地理解,当允许可变类型作为键时,我们通常使用 对象的状态作为键而不是对象本身。这将进一步强调@a_guest 的有效观点。
  • @rd11 感谢您的评论。已更新。
【解决方案3】:

hashset 计算对象的 hash 并基于该散列,将对象存储在结构中以便快速查找。因此,按照约定,一旦将对象添加到字典中,哈希就不允许更改。大多数好的散列函数将取决于元素的数量和元素本身。

元组是不可变的,所以在构造之后,值不能改变,因此哈希也不能改变(或者至少一个好的实现不应该让哈希改变)。

另一方面,列表是可变的:以后可以添加/删除/更改元素。因此,哈希可能会违反合同而改变。

因此,所有不能保证哈希函数在添加对象后保持稳定的对象都违反了合同,因此不是好的候选对象。因为对于lookup,字典会首先计算key的hash,并确定正确的bucket。如果键同时更改,这可能会导致误报:对象在字典中,但由于散列不同,因此无法再检索它,因此将搜索与最初添加对象的存储桶不同的存储桶.

【讨论】:

  • "[...] 一旦将对象添加到字典中,就不允许更改哈希值。"这是一个纯粹的定义问题,Python 刚刚决定不让列表可散列,因为这意味着什么并不明确。来自Python之禅:“面对歧义,拒绝猜测的诱惑。”但是,您可以从 Ruby 中看到这样的概念。
  • @a_guest:不,这也是效率问题。使用可变对象进行此操作的唯一方法是添加一个触发器,该触发器在对象被修改时重新散列该对象。大多数编程语言都反对这样做。但显然正如他们所说:计算机科学中的每个问题都可以通过另一层间接来解决
  • @a_guest:正如你所看到的here,你确实可以允许它,但它会产生各种容易出错并容易引入错误的副作用。
  • 仅仅因为一个对象是可变的并不意味着它的哈希值在修改时会改变。这完全取决于程序员(这意味着您不一定需要重新散列)。即使是相反的情况也是有道理的(修改后的哈希变化)。您只需要保证比较相等的两个对象具有相同的哈希值。或相反:具有不同哈希值的两个对象比较不相等。
  • @a_guest:当然,如果他们实现了元组来查看元素的id(..),那么它们可能包含可变元素。您唯一需要保证的是 hash 在添加后不会改变。您的最后两个陈述当然是散列的基础,但第二个实际上是后者的重言式:如果x =&gt; y 然后not y =&gt; not x (如果我没记错的话,古希腊人已经发现了)。
【解决方案4】:

我想添加以下方面,因为其他答案尚未涵盖。

使可变对象可散列并没有错,它只是不明确,这就是为什么它需要由程序员自己(而不是编程语言)一致地定义和实现。

请注意,您可以为任何自定义类实现 __hash__ 方法,该方法允许将其实例存储在需要可散列类型(例如字典键或集合)的上下文中。

哈希值通常用于确定两个对象是否代表同一事物。所以考虑下面的例子。您有一个包含两个项目的列表:l = [1, 2]。现在您将一个项目添加到列表中:l.append(3)。现在你必须回答以下问题:它还是一样的吗?两者 - 是和否 - 都是有效的答案。 “是”,还是同一个列表,“否”,不再是同一个内容了。

所以这个问题的答案取决于你作为程序员,所以你可以为你的可变类型手动实现哈希方法。

【讨论】:

  • 在Python中,什么时候计算dict哈希?如果仅在将键值对添加到 dict 时调用它,则定义 __hash__ 可能无济于事。
  • 我不明白你的意思。继续使用列表示例,以下内容有什么问题(除了效率):class MyList(list): def __hash__(self): return 0。具有与列表完全相同的功能,并满足哈希值的所有条件。您可以将它用作 dict 键而没有任何问题(除了 dict 访问将不再是 O(1) 但这是效率)。
  • 高效查找是字典的重点。其键都具有相同哈希的字典基本上是一个列表。我的问题是关于非常量哈希,例如sum(self)
【解决方案5】:

基于Python Glossary

如果一个对象的哈希值在其生命周期内永远不会改变(它需要一个 __hash__() 方法),并且可以与其他对象进行比较(它需要一个 __eq__() 方法),那么它就是可哈希的。比较相等的可散列对象必须具有相同的散列值。

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

【讨论】:

    【解决方案6】:

    因为列表是可变的,而元组不是。例如,当您将值的哈希存储在 dict 中时,如果对象发生更改,则存储的哈希值不会被发现,因此它将保持不变。下次您查找对象时,字典将尝试通过不再相关的旧哈希值查找它。

    为了防止这种情况,python 不允许你有可变的项目。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2016-09-05
      • 2023-01-14
      • 1970-01-01
      • 2010-12-29
      • 2013-06-13
      • 2011-09-12
      相关资源
      最近更新 更多