为什么迭代器和生成器是可哈希的?

4
作为标题。我的意思是,您可以调用next(obj)并指向下一个元素。因此,可迭代对象或生成器的内部状态会发生更改。
为什么它们是可哈希的?

它们通过身份哈希,这与它们的相等语义工作方式一致。 - juanpa.arrivillaga
内部状态可能会改变,但迭代器本身不会改变。 - chepner
@chepner:我不明白重点在哪里... - Marco Sulla
x = itertools.count(5) 为例。如果你调用 next(x),它将修改 x 的内部状态并返回 5。但是,下一次调用 next(x),你就知道它会返回 6;没有任何方法可以改变 x 本身来改变这个结果。从这个意义上说,x 是不可变的。 - chepner
count(5)是可哈希的,因为它的哈希值不依赖于迭代器的当前状态;无论next(x)返回6、19或其他什么值,都不能改变count(5)表示以5开始的无限整数序列的事实。相比之下,list的一个实例除了其当前内容外没有唯一的标识(因此也没有哈希值),因为对于任何两个列表xy,你都可以通过一系列的popappend调用使它们等价。 - chepner
2个回答

4
散列对象的通用规则是:
  1. 除非覆盖了 __eq__,否则对象的相等性由身份定义,并且散列匹配。
  2. 如果覆盖了 __eq__,但没有覆盖了 __hash__,那么默认情况下会阻止散列(因为影响等式检查结果的可变性将破坏散列不变量);重新启用散列需要实现 __hash__,其隐含地表示“我的等式和散列语义随时间稳定/一致”,但不要求与等式或散列码无关的内容稳定。
要点是,可散列的条件不是完全不可变性,而是与等式一致性(以及等式和散列的暗示稳定性)。由于大多数迭代器和所有生成器都不实现 __eq__(在不运行迭代器并且失去刚使用的用于比较的信息的情况下没有有意义的方式来实现它),因此所有基于身份的,就像任何未定义等式的用户定义对象一样。

好的,但是这样做可以使用迭代器、生成器、任何未定义__eq__的对象以及任何可变但实现了__hash__的对象作为dict的键。这是可取的吗? - Marco Sulla
没有人可以意外分享密钥。我没有理解这一点。 - Marco Sulla
@MarcoSulla:如果您需要在存储中创建一个新的键,保证不会与其他人现有的键重叠,您只需执行mynewkey = object()。由于object()(像所有没有覆盖__eq____hash__的对象一样)基于标识进行比较和哈希,所以新创建的object() 必须与所有现有的其他对象不同,并且与稍后可能创建的所有其他对象不同(如果所有对该对象的引用都消失,则可以创建一个新对象,该对象恰好重用内存,但数据存储本身防止对象消失),因此唯一的方法是... - ShadowRanger
什么?一个简单的递增整数不够用吗? - Marco Sulla
让我们在聊天中继续这个讨论 - ShadowRanger
显示剩余4条评论

0

虽然生成器的内部状态可以改变,但整个生成器在迭代时永远不能添加任何东西,也永远不能退回一步。因此,生成器是一个固定的不可变对象,这几乎就是可哈希性的定义。

但更深层次的是,甚至可变对象只要定义了__hash__作为实例方法,就可以被散列化,但这对可变对象来说很少是可取的。


2
具体来说,一个有效的__hash__实现将忽略可变对象的可变部分。 - chepner
那么一个对象实现__hash__的事实是该对象不可变的必要但不充分条件? - Marco Sulla
2
@MarcoSulla 不是的。一个对象成为不可变对象的唯一条件就是它没有暴露出任何改变其状态的方法。仅此而已。它不需要定义一个 __hash__ 方法。 - juanpa.arrivillaga
1
@MarcoSulla 更像是哈希计算,为了作为哈希值有效,不能依赖于可变状态。 - chepner
@MarcoSulla 不是的,任何对象(可变或不可变)都可以实现__hash__,不可变的定义不同,它是一个不能“原地”更改并且对其进行的任何更改都必须替换它的对象。迭代生成器并不会改变生成器,它只是逐个读取它。 - Ofer Sadan
2
@OferSadan:我认为这不是一个有用的论点。显然,在迭代后,生成器是不同的;试图声称它根本没有改变是站不住脚的。重要的是,它的状态不会以任何影响相等比较的方式发生改变;它仍然等于自己,而不等于*所有其他东西。 - ShadowRanger

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接