为啥浮点字典键可以覆盖具有相同值的整数键?

Posted

技术标签:

【中文标题】为啥浮点字典键可以覆盖具有相同值的整数键?【英文标题】:Why can a floating point dictionary key overwrite an integer key with the same value?为什么浮点字典键可以覆盖具有相同值的整数键? 【发布时间】:2015-11-19 11:04:58 【问题描述】:

我正在处理http://www.mypythonquiz.com,question #45 要求输出以下代码:

confusion = 
confusion[1] = 1
confusion['1'] = 2
confusion[1.0] = 4

sum = 0
for k in confusion:
    sum += confusion[k]

print sum

输出为6,因为1.0 替换了1。这对我来说有点危险,这是一个有用的语言功能吗?

【问题讨论】:

使用内置的sum 作为变量也(稍微)令人困惑。即使语言允许,我也不相信尝试将浮点数用作 int 键的代码,即使我确信在某些情况下它可能有用。 这只会在您认为1.01 不同的情况下伤害您。鉴于这些情况(表面上)很少见,默认行为是平等对待它们是可以理解的。 @JohnColeman 我会怀疑包含不相交类型作为键的 any 字典。 @MarkRansom 好点。无论如何,99% 的时间我只是使用字符串或整数(或由它们构建的各种元组)。我真的不相信浮动键。 @Bakuriu 您的标题编辑改变了问题的精神,我的兴趣主要是这种行为是否有用/它如何与 Python 语言的其余部分相适应,而不仅仅是“为什么”这样行为发生。尽管如此,关于最初的问题,还是出现了许多好的答案。 【参考方案1】:

坦率地说,相反是危险的! 1 == 1.0,因此不难想象,如果您让它们指向不同的键并尝试根据评估的数字访问它们,那么您可能会遇到麻烦,因为模糊性很难弄清楚。

动态类型意味着值比某事物的技术类型更重要,因为类型具有延展性( 是一个非常有用的特性),因此区分 ints 和 @ 987654323@ 与 distinct 相同的值是不必要的语义,只会导致混淆。

【讨论】:

给定 x=1.0 和 y=1,x*123456789*123456789*123456789 和 y*123456789*123456789*123456789 会产生相同的值还是不同的值?虽然有一个“数学”相等运算符报告 1.0 和 1 表示相同的值是有意义的,但一个好的语言还应该有一种比较方法,可以将它们识别为不同的。 @supercat Python 确实有办法区分这两者,只是在确定哈希值时没有使用它。【参考方案2】:

在python中:

1==1.0
True

这是因为隐式转换

但是:

1 is 1.0
False

我明白为什么floatint 之间的自动转换很方便,将int 转换为float 相对安全,但还有其他语言(例如go)远离隐式转换.

这实际上是一个语言设计决定,并且比不同的功能更多的是品味问题

【讨论】:

is 带有数字不是一个好主意...例如在x=1000000 之后,表达式x is 1000000False【参考方案3】:

您应该考虑到dict 旨在根据逻辑数值存储数据,而不是根据您的表示方式。

ints 和floats 之间的区别确实只是一个实现细节,而不是概念上的。理想情况下,唯一的数字类型应该是具有无限精度甚至亚单位精度的任意精度数字......然而,这很难在不遇到麻烦的情况下实现......但这可能是 Python 未来唯一的数字类型。

因此,尽管出于技术原因 Python 会尝试隐藏这些实现细节,但 int->float 的转换是自动的。

如果在 Python 程序中,x 是值为 1 的 float 时不使用 if x == 1: ...,那将更加令人惊讶。

请注意,在 Python 3 中,1/2 的值也是 0.5(两个整数的除法),并且类型 long 和非 unicode 字符串已被删除,同样试图隐藏实现细节。

【讨论】:

int->float Promotion 应该在必要的情况下是自动的,但在这种情况下,我认为不是。当您尝试将 int 放入超出 float 范围或无法往返的字典时会发生什么? “整数和浮点数之间的区别确实只是一个实现细节,而不是概念上的。” 我不同意(尽管整数和长整数确实如此)。整数和浮点数具有明显不同的除法行为(如您所述),只有整数提供 .bit_length() 方法。浮点数也不允许用作数组索引——如果它们应该是它们应该实现 __index__ 并且只对非整数值引发错误。这些绝对是概念上的差异,而不仅仅是实现上的差异。 @JeremyBanks:除法的区别是 Python 2.x 中的一个“设计错误”,它无法为向后兼容而修复,并且已尽快修复(即在 Python 3.x 中)。我想说的是,您不能将3.0 用作数组索引这一事实是一个错误而不是一个功能,但可能是 Guido 不同意这一点。当然存在差异(例如 type(3)type(3.0) 不一样)......关键是它们是否是偶然的差异(我们很想摆脱)或者它们是否是想要的 差异... @6502:您还希望能够使用3.000000000000001 作为数组索引吗?还是2.999999999999999,还是3.141592653589793?如果不是,我认为你也不应该对3.0 感到满意。 @leftaroundabout: 如果你喜欢intfloat 是不同的类型,那么你也不应该对3 == 3.0 感到满意;但这是 IMO 非常烦人的(即使 OCaml 的人认为不同)。如果3 == 3.0x[3] 也应该与x[3.0] 相同。另一方面,3.0000000001 有所不同,它引发错误可能有助于调试问题。顺便说一句,双精度数可以完全表示绝对值小于 2^53 的所有整数...即 9,007,199,254,740,992(我们不会有很长一段时间)。【参考方案4】:

我同意其他人的观点,在这种情况下将11.0 视为相同是有意义的。即使 Python 确实以不同的方式对待它们,尝试使用 11.0 作为字典的不同键也可能是个坏主意。另一方面——我很难想到在键的上下文中使用1.0 作为1 的别名的自然用例。问题是密钥要么是文字,要么是计算出来的。如果它是文字键,那么为什么不直接使用1 而不是1.0?如果它是一个计算键——舍入错误可能会搞砸:

>>> d = 
>>> d[1] = 5
>>> d[1.0]
5
>>> x = sum(0.01 for i in range(100)) #conceptually this is 1.0
>>> d[x]
Traceback (most recent call last):
  File "<pyshell#12>", line 1, in <module>
    d[x]
KeyError: 1.0000000000000007

所以我想说,一般来说,你的问题的答案是“这是一个有用的语言功能吗?”是“不,可能不是。”

【讨论】:

【参考方案5】:

首先:hash 函数的文档中明确记录了该行为:

hash(object)

返回对象的哈希值(如果有的话)。哈希值是 整数。它们用于在搜索过程中快速比较字典键 字典查找。 比较相等的数值具有相同的 哈希值(即使它们是不同的类型,1 的情况也是如此 和1.0)。

其次,object.__hash__ 的文档中指出了散列的限制

object.__hash__(self)

由内置函数 hash() 调用并用于对成员的操作 散列集合,包括 setfrozensetdict. __hash__() 应该返回一个整数。 唯一需要的属性是对象 比较相等的具有相同的哈希值;

这不是python独有的。 Java 也有同样的警告:如果您实现 hashCode,那么为了让事情正常工作,您必须以这样的方式实现它:x.equals(y) 隐含 x.hashCode() == y.hashCode()

因此,python 决定 1.0 == 1 成立,因此 强制hash 提供一个实现,以便 hash(1.0) == hash(1)。副作用是 1.01 的行为方式与 dict 键完全相同,因此行为。

换句话说,行为本身不必以任何方式使用或有用。 有必要。如果没有这种行为,在某些情况下您可能会意外覆盖不同的密钥。

如果我们有1.0 == 1hash(1.0) != hash(1),我们仍然可能会发生冲突。如果1.01 发生冲突,dict 将使用相等来确定它们是否是相同的键,并且 kaboom 即使您希望它们不同,该值也会被覆盖.

避免这种情况的唯一方法是使用1.0 != 1,以便dict 能够在发生冲突的情况下区分它们。但人们认为拥有1.0 == 1 比避免您所看到的行为更重要,因为您实际上从不使用floats 和ints 作为字典键。

由于 python 试图通过在需要时自动转换数字来隐藏数字之间的区别(例如1/2 -&gt; 0.5),因此即使在这种情况下也会反映这种行为是有道理的。它与python的其余部分更加一致。


此行为将出现在 any 实现中,其中键的匹配至少部分(如在哈希映射中)基于比较。

例如,如果 dict 是使用红黑树或其他类型的平衡 BST 实现的,则在查找键 1.0 时,与其他键的比较将返回与 1 相同的结果所以他们仍然会以同样的方式行事。

哈希映射需要更加小心,因为它是用于查找键条目的哈希值,并且比较仅在之后进行。因此,违反上述规则意味着您将引入一个很难发现的错误,因为有时dict 可能看起来像您预期的那样工作,而在其他时候,当大小发生变化时,它会开始行为不正确。


请注意,有一种方法可以解决此问题:为字典中插入的每种类型都有一个单独的哈希映射/BST。这样,不同类型的对象之间就不会发生任何冲突,并且当参数具有不同类型时,== 的比较方式无关紧要。

但是这会使实现复杂化,它可能效率低下,因为哈希映射必须保留相当多的空闲位置才能获得 O(1) 访问时间。如果它们变得太满,性能就会下降。拥有多个哈希映射意味着浪费更多空间,而且您需要先选择要查看的哈希映射,然后才能开始实际查找密钥。

如果您使用 BST,您首先必须查找类型并执行第二次查找。因此,如果您要使用多种类型,您最终会得到两倍的工作量(并且查找需要 O(log n) 而不是 O(1))。

【讨论】:

我认为这可以更好地解释正在发生的事情 我发现关于散列的讨论完全无关紧要。这是一个实现细节,一个为每个值返回42 的哈希函数将是一个有效的尽管低效的哈希(因此您永远无法根据哈希值决定任何事情)。关键是在 Python 3 == 3.0 中,该字典适用于相等性。 @6502 我在最后添加了几段。看看他们是否满足你。无论如何,由于 OP 询问的是 python 的 dict 而不是映射的通用概念,我认为散列 is 确实相关。 字典是一个键/值存储,不能包含两个具有相同键的条目。因此,由于 1 和 1.0 相等,它不能同时包含两者。在那里,解释它没有解释哈希表是如何工作的...... @6502 但它是一个重要的实现细节,具有巨大的实际影响。有时会处理 3==3.0 有时不会处理的字典会是一件坏事,并且阻止它强制您给它们相同的哈希值。【参考方案6】:

字典是用哈希表实现的。要在哈希表中查找某些内容,请从哈希值指示的位置开始,然后搜索不同的位置,直到找到相等的键值或空桶。

如果您有两个比较相等但具有不同哈希值的键值,您可能会得到不一致的结果,具体取决于另一个键值是否在搜索的位置中。例如,随着桌子满了,这更有可能发生。这是您要避免的事情。 Python 开发人员似乎考虑到了这一点,因为内置的 hash 函数为等效数值返回相同的哈希值,无论这些值是 int 还是 float。请注意,这扩展到其他数字类型,False 等于 0 并且 True 等于 1。甚至fractions.Fractiondecimal.Decimal 都支持这个属性。

如果a == bhash(a) == hash(b) 的要求记录在object.__hash__() 的定义中:

由内置函数hash() 调用,用于对散列集合成员的操作,包括setfrozensetdict__hash__() 应该返回一个整数。唯一需要的属性是比较相等的对象具有相同的哈希值;建议以某种方式混合在一起(例如,使用异或)对象组件的哈希值,这些组件也参与对象的比较。

TL;DR:如果比较相等的键没有映射到相同的值,字典就会损坏。

【讨论】:

以上是关于为啥浮点字典键可以覆盖具有相同值的整数键?的主要内容,如果未能解决你的问题,请参考以下文章

将字典中具有相同值的所有键组合在一起,并将键与值交换,将值与键交换

Python:总结具有不同键和相同值的字典列表

比较两个字典(键,值)并返回不具有相同值的键

python 中关于字典的键

使用 Swift 将具有相同类型的字典分组到具有完整键和值的数组中

使用 scipy.io.loadmat 从 .mat Matlab 文件中将字典键转换为 Python 中具有相同值的变量名