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

Why can a floating point dictionary key overwrite an integer key with the same value?

我正在处理 http://www.mypythonquiz.com, and 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。这对我来说有点危险,这是一个有用的语言功能吗?

老实说,对面很危险! 1 == 1.0,所以可以想象,如果你让它们指向不同的键并尝试根据评估的数字访问它们,那么你可能 运行 会遇到麻烦,因为歧义很难弄清楚。

动态类型意味着值比事物的技术类型更重要,因为类型是可塑的(一个非常有用的特性),所以区分两者intsfloats 具有与 distinct 相同的值是不必要的语义,只会导致混淆。

在python中:

1==1.0
True

这是因为隐式转换

但是:

1 is 1.0
False

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

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

你应该考虑到 dict 旨在根据逻辑数值存储数据,而不是你如何表示它。

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

因此,虽然由于技术原因具有不同的类型,Python 试图隐藏这些实现细节,并且 int->float 转换是自动的。

如果在 Python 程序中当 x 是一个值为 1 的 floatif x == 1: ... 不会被采用,那就更令人惊讶了。

请注意,对于 Python 3,1/2 的值也是 0.5(两个整数相除)并且类型 long 和非 unicode 字符串已因隐藏实施细节的相同尝试而被删除。

我同意其他人的观点,在这种情况下将 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

所以我想说,一般来说,你的问题 "is this ever a useful language feature?" 的答案是 "No, probably not."

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

hash(object)

Return the hash value of the object (if it has one). Hash values are integers. They are used to quickly compare dictionary keys during a dictionary lookup. Numeric values that compare equal have the same hash value (even if they are of different types, as is the case for 1 and 1.0).

其次,object.__hash__

的文档中指出了散列的局限性

object.__hash__(self)

Called by built-in function hash() and for operations on members of hashed collections including set, frozenset, and dict. __hash__() should return an integer. The only required property is that objects which compare equal have the same hash value;

这不是 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 -> 0.5),因此即使在这种情况下也会反映出这种行为是有道理的。它与 python.

的其余部分更加一致

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

例如,如果 dict 是使用红黑树或其他类型的平衡 BST 实现的,当查找键 1.0 时,与其他键的比较将 return 与 1 相同的结果,因此它们仍将以相同的方式运行。

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


请注意, 可以解决此问题:为字典中插入的每种类型设置单独的散列 map/BST。这样,不同类型的对象之间就不会发生任何冲突,当参数具有不同类型时,== 比较的方式也无关紧要。

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

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

字典是用散列 table 实现的。要在散列 table 中查找内容,您从散列值指示的位置开始,然后搜索不同的位置,直到找到一个相等的键值或一个空桶。

如果您有两个比较相等但哈希值不同的键值,您可能会得到不一致的结果,具体取决于另一个键值是否在搜索位置。例如,当 table 变满时,这种情况更有可能发生。这是您要避免的事情。 Python 开发人员似乎考虑到了这一点,因为内置 hash 函数 returns 相同的哈希值用于等效数值,无论这些值是否 intfloat。请注意,这扩展到其他数字类型,False 等于 0True 等于 1。就连fractions.Fractiondecimal.Decimal也坚持这个属性.

if a == b then hash(a) == hash(b) 的要求记录在 object.__hash__() 的定义中:

Called by built-in function hash() and for operations on members of hashed collections including set, frozenset, and dict. __hash__() should return an integer. The only required property is that objects which compare equal have the same hash value; it is advised to somehow mix together (e.g. using exclusive or) the hash values for the components of the object that also play a part in comparison of objects.

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