我正在尝试获取 lambda 函数的哈希值。为什么我得到两个值(8746164008739 和 -9223363290690767077)?为什么 lambda 函数的散列值不总是一个值?
>>> fn = lambda: 1
>>> hash(fn)
-9223363290690767077
>>> fn = lambda: 1
>>> hash(fn)
8746164008739
>>> fn = lambda: 1
>>> hash(fn)
-9223363290690767077
>>> fn = lambda: 1
>>> hash(fn)
8746164008739
>>> fn = lambda: 1
>>> hash(fn)
-9223363290690767077
两个对象不能保证散列为相同的值,除非它们比较等于 [1]。
Python 函数(包括 lambda)即使具有相同的代码 [2] 也不会比较相等。例如:
>>> (lambda: 1) == (lambda: 1)
False
实现方面,这种行为是由于函数对象不提供自己的相等运算符。相反,它们继承了使用对象标识的默认标识,即它的地址。来自文档:
如果没有定义
、__cmp__()
或__eq__()
操作,类 实例通过对象标识(“地址”)进行比较。__ne__()
这是您的特定示例中发生的情况:
fn = lambda: 1 # New function is allocated at address A and stored in fn.
fn = lambda: 1 # New function is allocated at address B and stored in fn.
# The function at address A is garbage collected.
fn = lambda: 1 # New function is allocated at address A and stored in fn.
# The function at address B is garbage collected.
fn = lambda: 1 # New function is allocated at address B and stored in fn.
# The function at address A is garbage collected.
...
由于地址
A
总是散列为一个值,地址 B
为另一个值,您会看到 hash(fn)
在两个值之间交替。然而,这种交替行为是一种实现人工制品,并且有一天可能会改变,例如,如果让垃圾收集器的行为略有不同。
以下有见地的注释由@ruakh 提供:
值得注意的是,不能写出一个通用的流程 用于确定两个函数是否等价。 (这是一个 停机问题的不可判定性的后果。) 此外,两个 Python 函数的行为可能不同,即使它们 代码是相同的(因为它们可能是引用的闭包 不同但名称相同的变量)。所以这是有道理的 Python 函数不会重载相等运算符:没有办法 实现比默认对象标识更好的东西 比较。
[1] 反之通常不成立:比较不相等的两个对象可以具有相同的哈希值。这称为哈希冲突。
[2] 调用你的 lambdas 然后散列结果当然会总是给出相同的值,因为
hash(1)
在一个程序中总是相同的:
>>> (lambda: 1)() == (lambda: 1)()
True
lambda
函数对象的散列基于其内存地址(在 CPython 中,这是 id
函数返回的内容)。这意味着即使函数包含相同的代码,任何两个函数对象都将具有不同的哈希值(假设没有哈希冲突)。
为了解释问题中发生的事情,首先请注意编写
fn = lambda: 1
会在内存中创建一个新的函数对象并将名称fn
绑定到它。因此,这个新函数将具有与任何现有函数不同的哈希值。
重复
fn = lambda: 1
,您会得到哈希的交替值,因为当fn
绑定到newly创建的函数对象时,fn
previously指向的函数是Python收集的垃圾。这是因为不再有任何对它的引用(因为名称 fn
现在指向不同的对象)。
Python 解释器然后将这个旧内存地址重新用于下一个通过编写
fn = lambda: 1
. 创建的新函数对象
此行为可能因不同的系统和 Python 实现而异。
确定两个函数是否相等是不可能的,因为它是停机问题的超集。
在理想世界中,比较(并因此散列)函数会导致类型错误。 Python 显然不喜欢这样,而是选择使用函数的标识来比较(并因此散列)它们。
每次你做
fn = lambda: 1
一个新的函数对象被创建,旧对象绑定到名称fn
被标记为删除。但 Python 并不简单地释放对象,将其内存传递回操作系统。为了最小化内存分配的系统调用和最小化内存碎片,Python 会尽可能地回收内存。因此,当您第三次创建 fn = lambda: 1
时,解释器会注意到它手边有一块足够大的 RAM,足以容纳新的函数对象,因此它会使用该块。因此,您的第三个fn
最终位于该RAM块中,因此与第一个fn
具有相同的id,因为CPython对象的id是它们的内存地址。
(正如其他人所提到的,任何不提供
__hash__
特定实现的对象类型的散列是基于它在 CPython 中的 id。如果一个类没有定义 __cmp__
或 __eq__
方法,它应该也没有定义 __hash__
操作)。
在 python3 中获取足够的 lambda 内容的依赖于实现的方法:
def function_contents(func):
closure = tuple(cell.cell_contents for cell in func.__closure__) if func.__closure__ else ()
return (func.__name__, func.__defaults__, closure, func.__code__.co_code, func.__code__.co_consts)
这包括:
应该足以满足您的用例。
你的例子:
fn = lambda: 1
hash(function_contents(fn))
fn = lambda: 1
hash(function_contents(fn))
fn = lambda: 1
hash(function_contents(fn))
...每次都相同的值。
hash_func = lambda input_str: hashlib.sha256(input_str.encode()).hexdigest()