Python浮点数冷知识:NaN与Infinity的哈希陷阱
本文探讨Python中浮点数的两个反直觉现象:相等元组的元素可能不相等,以及不相等的NaN对象哈希值却相同。深入解析float()对'nan'和'inf'的解析规则,揭示NaN作为非数值在比较、哈希及字典键使用中的特殊行为,并揭秘解释器中硬编码的哈希彩蛋。
Python浮点数冷知识:NaN与Infinity的哈希陷阱
本周PyCoder's Weekly推荐了一篇趣味短文,其中涉及的浮点数特性颇具启发性,在此进行补充并分享。
文中提出的几个问题值得读者先行思考:
- 如果两个元组完全相等,即满足 a==b 且 a is b,那么它们相同索引处的元素(例如 a[0] 与 b[0])是否一定相等?
- 如果两个对象的哈希值相同,即 hash(a) == hash(b),这是否意味着这两个对象必然相等?
上述两个问题的答案均为否定,否则便不构成所谓的“冷知识”。建议读者先尝试给出自己的解答,随后再查阅下文分析。
-----思考分割线-----
相等元组的元素为何不相等?
首先分析第一个问题。对于两个相同的元组 a 和 b,它们之间存在如下关系:
>>> a = (float('nan'),)
>>> b = a
>>> a # (nan,)
>>> b # (nan,)
>>> type(a), type(b)
(<type 'tuple'>, <type 'tuple'>)
>>> a == b
True
>>> a is b # 即 id(a) == id(b)
True
>>> a[0] == b[0]
False
上述代码执行结果显示:a 与 b 相等(类型、值及内存ID均一致),但它们对应的元素却不相等。
这两个元组仅包含一个元素(逗号后无其他元素,此为单元素元组的语法表示,即 len(a)==1 )。float() 作为内置函数,用于将参数转换为浮点数。
造成这一现象的原因需查阅文档,该内置函数的解析逻辑如下:
sign ::= "+" | "-"
infinity ::= "Infinity" | "inf"
nan ::= "nan"
numeric_value ::= floatnumber | infinity | nan
numeric_string ::= [sign] numeric_value
在解析过程中,它可以处理前后空格、正负号前缀(+/-)以及常规浮点数。此外,它还支持两类不区分大小写的字符串:“Infinity”或“inf”,代表无穷大;以及“nan”,代表非数值(not-a-number),确切地说,它指代除常规数值外的所有情况。
前文提到的第一个冷知识便与“nan”相关。尽管两个元组整体相等,但其唯一元素却不相等。原因在于“nan”代表非数值概念,是一个范围而非具体值,因此不具备可比性。
作为对比,我们观察两个“无穷大浮点数”的表现:
>>> a = (float('inf'),)
>>> b = a
>>> a # (inf,)
>>> b # (inf,)
>>> a == b # True
>>> a is b # True
>>> a[0] == b[0] # True
请注意最后一次比较,其结果与前两个元组恰好相反。由此可得出结论:两个无穷大浮点数数值相等,而两个“非数值”对象数值不相等。
简化上述逻辑,可表述为:
>>> a = float('inf')
>>> b = float('inf')
>>> c = float('nan')
>>> d = float('nan')
>>> a == b # True
>>> c == d # False
不相等对象的哈希值为何相同?
至此,第一个冷知识已揭秘。接着探讨第二个问题:
>>> hash(float('nan')) == hash(float('nan'))
True
前文指出两个“非数值”对象不相等,但此处显示它们的哈希结果却相等,这似乎违背直觉。
我们可以推导出一条简明结论:两个不相等的对象,其哈希结果可能相同。
原因在于 hash(float('nan')) 的结果固定为 0,这是一个恒定值,因此在比较时必然相等。
实际上,关于 hash() 函数还隐藏着一个彩蛋:
>>> hash(float('inf')) # 314159
>>> hash(float('-inf')) # -314159
这个数值是否似曾相识?它正是圆周率 π 的前五位 3.14159 去掉小数点后的结果。在早期Python版本中,负无穷大的哈希值实为 -271828,取自自然对数 e 的前几位。这两个数值均硬编码于Python解释器中,可谓一种技术致敬。
由于 float('nan') 的哈希值相同,通常意味着它们不能作为字典中不同的键,但实际情况却出人意料:
>>> a = {float('nan'): 1, float('nan'): 2}
>>> a
{nan: 1, nan: 2}
# 作为对比:
>>> b = {float('inf'): 1, float('inf'): 2}
>>> b
{inf: 2}
如上所示,两个 nan 键在表示上完全一致(注意未加引号),它们可以共存;而 inf 则会被合并为一个,再次展现了 nan 的特殊性。
至此,两个冷知识分享完毕。其根本原因在于 float() 在解析浮点数时,Python 允许了 nan(非数值)的存在,这种不确定的存在导致了上述奇怪结果。
最后,我们进行总结:
- 包含 float('nan') 的两个元组,作为整体比较时结果相等;但这两个相等元组的对应元素可能不相等。
- float('nan') 代表“非数值”,其本身非确定值,两个对象比较时不相等,但其哈希值为固定值(0),比较时相等;它可用作字典键,且互不冲突。
- float('inf') 代表无穷大浮点数,可视为确定值,两个对象比较时相等,其哈希值也相等;它可用作字典键,但会产生冲突(后值覆盖前值)。
- float('nan') 的哈希结果为 0,float('inf') 的哈希结果为 314159。
参考资料:
https://docs.python.org/3/library/functions.html#float
https://www.pythondoeswhat.com/2019/09/welcome-to-float-zone.html
总结
以上就是本文的全部内容,希望这些内容对您的学习或工作有所帮助,感谢支持。
本站声明
本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。













