【问题标题】:Why and how are Python functions hashable?Python 函数为什么以及如何是可散列的?
【发布时间】:2016-11-25 21:48:58
【问题描述】:

我最近在 Python 中尝试了以下命令:

>>> {lambda x: 1: 'a'}
{<function __main__.<lambda>>: 'a'}

>>> def p(x): return 1
>>> {p: 'a'}
{<function __main__.p>: 'a'}

dict 的成功创建表明 lambda 和常规函数都是可散列的。 ({[]: 'a'} 之类的失败,TypeError: unhashable type: 'list')。

哈希显然不一定是函数的ID:

>>> m = lambda x: 1
>>> id(m)
140643045241584
>>> hash(m)
8790190327599
>>> m.__hash__()
8790190327599

最后一条命令显示__hash__ 方法是为lambdas 显式定义的,也就是说,这不是 Python 根据类型计算的自动魔法。

使函数可散列化的动机是什么?另外,函数的哈希值是多少?

【问题讨论】:

  • 我真的觉得这是你在对“你为什么不呢?”有一个好的答案之前不应该考虑的问题。
  • @Hurkyl。因为它需要为一件事增加额外的维护负担。有人必须设计和编写__hash__ 函数,所以他们清楚地看到了它的好处。我想知道是什么让他们不只是不管它。
  • 虽然给出了答案,但似乎禁用散列函数需要更多的工作,而不仅仅是从对象继承它。所以事实上,其中一个考虑因素可能是减少维护债务。
  • @MadPhysicist,无论哪种方式,代码都很简单。在C 中,对象的类型由struct PyTypeObject 表示,该结构的tp_hash 成员定义__hash__() 发生的情况。如果该成员初始化为 0,则它继承 __hash__()。如果您希望该类型拒绝散列,只需将其初始化为 PyObject_HashNotImplemented 即可。 (而且如果类型要实现自定义散列,你插入类型的自定义散列函数的地址。)所以维护负担真的与它无关。

标签: python hash


【解决方案1】:

没什么特别的。如您检查函数类型的未绑定__hash__ 方法所见:

>>> def f(): pass
...
>>> type(f).__hash__
<slot wrapper '__hash__' of 'object' objects>

of 'object' objects 部分意味着它只是从object 继承了默认的基于身份的__hash__。函数==hash 按身份工作。 idhash 之间的差异对于任何继承 object.__hash__ 的类型都是正常的:

>>> x = object()
>>> id(x)
40145072L
>>> hash(x)
2509067

您可能认为__hash__ 只应该为不可变对象定义,您几乎是对的,但它缺少一个关键细节。 __hash__ 应该只为 == 比较中涉及的所有内容 不可变的对象定义。对于== 基于身份的对象,将hash 基于身份也是完全标准的,因为即使对象是可变的,它们也不可能以改变其身份的方式可变。具有基于身份的== 的文件、模块和其他可变对象都以这种方式运行。

【讨论】:

  • 嗯?我想这解释了“如何”。但是为什么?
  • @wallyk:我已经详细说明了原因。
  • 所以默认情况下,Python 中的所有实例都是可散列的,除非它在类层次结构中的某处被关闭?这确实是个有趣的消息,尽管直观上看它是有道理的
  • @Delioth: == 对任何未指定其他行为的类型按身份工作。
  • @MadPhysicist 告诉python类不可散列的正确方法是将__hash__设置为Noneclass X: __hash__ = None;{X(): 1} -&gt; TypeError: unhashable type: 'X'
【解决方案2】:

它可能很有用,例如,创建函数对象集,或按函数索引 dict。不可变对象通常支持__hash__。在任何情况下,deflambda 定义的函数之间没有内部差异 - 这纯粹是句法。

使用的算法取决于 Python 的版本。看起来您正在 64 位机器上使用最新版本的 Python。在这种情况下,函数对象的散列是其id() 的右旋转 4 位,结果被视为带符号的 64 位整数。完成右移是因为对象地址(id() 结果)通常是对齐的,因此它们的最后 3 或 4 位始终为 0,这对于散列函数来说是一个有点烦人的属性。

在您的具体示例中,

>>> i = 140643045241584 # your id() result
>>> (i >> 4) | ((i << 60) & 0xffffffffffffffff) # rotate right 4 bits
8790190327599  # == your hash() result

【讨论】:

  • @JulienBernu,查看@user2357112 的回复 - 就== 而言,它是不可变的,这是__hash__() 所关心的。
【解决方案3】:

函数是可散列的,因为它是一个普通的、内置的、不可变的对象。

来自Python Manual

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

哈希性使对象可用作字典键和集合成员,因为这些数据结构在内部使用哈希值。

Python 的所有不可变内置对象都是可散列的,而没有可变容器(例如列表或字典)是可散列的。默认情况下,作为用户定义类实例的对象是可散列的;它们都比较不相等(除了它们自己),它们的哈希值来自它们的id()

【讨论】:

  • 它们实际上比你想象的更易变。您可以为它们添加属性,重新分配他们的__name____module____doc__,如果您感觉很疯狂,甚至可以重新分配他们的__code__
  • 你也可以重新分配他们的__hash__函数:)
  • @MadPhysicist:虽然重新分配 f.__hash__ 实际上不会对 hash(f) 做任何事情。
  • @user2357112 很有趣。我刚刚确认。这是为什么呢?
猜你喜欢
  • 2015-10-10
  • 2020-03-30
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-09-12
  • 2010-12-29
相关资源
最近更新 更多