为什么浮点字典键可以用相同的值覆盖整数键?
我正在通过http://www.mypythonquiz.com工作, 问题#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
。 这对我来说有点危险,这是否是一个有用的语言function?
您应该考虑dict
目的是根据逻辑数值来存储数据,而不是如何表示它。
int
s和float
s的区别实际上只是一个实现细节而不是概念。 理想情况下,唯一的数字types应该是一个任意的精确数字,具有无限的准确性,即使是子统一性…但是这样做很难实现而不会陷入困境……但是这可能是Python将来唯一的数字types。
所以,虽然有不同types的技术原因,Python试图隐藏这些实现细节,并且int
– > float
转换是自动的。
如果在Python程序中if x == 1: ...
不会被采用,那么当x
是一个值为1的float
时,会更令人惊讶。
请注意,对于Python 3,1 1/2
2的值是0.5
(两个整数的除法),并且typeslong
和非unicodestring已被放弃,同样尝试隐藏实现细节。
首先:行为在散列函数的文档中明确logging:
hash(object)
返回对象的散列值(如果有的话)。 哈希值是整数。 它们用于在字典查找期间快速比较字典键。 比较相等的数值具有相同的散列值(即使它们是不同的types,就像
1
和1.0
)。
其次,哈希的限制在object.__hash__
哈希的文档中被指出
object.__hash__(self)
由内置函数
hash()
调用,并用于散列集合(包括set
,frozenset
和dict. __hash__()
hash()
成员的操作dict. __hash__()
dict. __hash__()
应该返回一个整数。 唯一需要的属性是比较相等的对象具有相同的散列值;
这不是python独有的。 Java有相同的警告:如果你实现hashCode
然后,为了正常工作,你必须以这样的方式实现它: x.equals(y)
意味着x.hashCode() == y.hashCode()
。
所以,python决定1.0 == 1
成立,因此它被迫提供一个hash(1.0) == hash(1)
。 副作用是1.0
和1
作用与dict
键完全相同,因此是行为。
换句话说,行为本身不必以任何方式被使用或有用。 这是必要的 。 如果没有这种行为,就会出现你可能无意中覆盖不同的密钥的情况。
如果我们有1.0 == 1
但hash(1.0) != hash(1)
我们仍然可能有碰撞 。 如果1.0
和1
碰撞, dict
将使用相等性来确定它们是否是相同的密钥,并且即使您希望它们不同, kaboom的值也会被覆盖。
避免这种情况的唯一方法是让1.0 != 1
,这样即使在碰撞的情况下, dict
也能够区分它们。 但是1.0 == 1
被认为比避免你所看到的行为更重要,因为你几乎从不使用float
s和int
s作为字典键。
由于python试图通过在需要时自动转换它们来隐藏数字之间的区别(例如1/2 -> 0.5
),因此即使在这种情况下也反映出这种行为是有意义的。 它与Python的其余部分更加一致。
这种行为将出现在任何实现中,其中键的匹配至less部分地(如在哈希映射中)基于比较。
例如,如果使用红黑树或其他平衡BST实现dict
,当查找键1.0
时,与其他键的比较将返回与1
相同的结果,因此它们仍将在相同办法。
哈希映射需要更多的关注,因为它是用来查找密钥条目的哈希值,只有在之后才进行比较。 因此,打破上面提到的规则意味着你会引入一个很难find的错误,因为有时dict
可能会像你期望的那样工作,而在其他时候,当尺寸改变时,它会开始performance不正确。
请注意,有一种方法可以解决这个问题:对字典中插入的每种types都有单独的哈希映射/ BST。 通过这种方式,不同types的对象之间不会有任何冲突,当参数具有不同的types时, ==
如何比较并不重要。
然而,这会使实现复杂化,因为哈希映射必须保留相当多的空闲位置才能有O(1)个访问时间,所以这可能是低效的。 如果他们太满了,表演会减less。 拥有多个哈希映射意味着浪费更多空间,并且在开始实际查找关键字之前,您还需要先select要查看的哈希映射。
如果您使用BST,您首先必须查找types并执行第二次查找。 所以如果你打算使用很多types的话,你最终会得到两倍的工作量(查找将会花费O(log n)而不是O(1))。
在python中:
1==1.0 True
这是因为隐式投射
然而:
1 is 1.0 False
我可以看到为什么float
和int
之间的自动转换是方便的,将int
转换为float
是相对安全的,但是还有其他语言(比如go)远离隐式转换。
这实际上是一个语言devise的决定,而不仅仅是不同的function
字典是用哈希表来实现的。 要在散列表中查找某些内容,请从散列值指定的位置开始,然后search不同的位置,直到find等于或空的存储区的键值。
如果您有两个比较相等的密钥值,但具有不同的哈希值,则可能会得到不一致的结果,具体取决于其他键值是否位于search位置。 例如,当表格变满时,这将更有可能。 这是你想要避免的。 看来,Python开发人员有这个想法,因为内置的hash
函数返回相同的哈希等价数值,无论这些值是int
还是float
。 请注意,这扩展到其他数字types, False
等于0
, True
等于1
。 即使fractions.Fraction
和decimal.Decimal
维护这个属性。
如果a == b
那么hash(a) == hash(b)
被logging在object.__hash__()
的定义中。
由内置函数
hash()
调用,并用于散列集合(包括set
,frozenset
和dict
hash()
成员的操作。__hash__()
应该返回一个整数。 唯一需要的属性是比较相等的对象具有相同的散列值; build议以某种方式混合在一起(例如使用排他或)散列值的对象的组成部分,也发挥作用的比较对象的一部分。
TL; DR:如果比较相等的键没有映射到相同的值,字典将会中断。
坦率地说,相反是危险的! 1 == 1.0
,所以想象一下,如果你让他们指向不同的键,并试图根据评估的数字来访问它们,那么你很可能会遇到麻烦,因为含糊不清是很难理解的。
dynamictypes意味着这个值比什么是技术types更重要,因为types是可延展的(这是一个非常有用的特性),所以区分同一个值的ints
和floats
值是不同的,这是不必要的语义导致混乱。
我同意其他人的观点,认为在这种情况下, 1
和1.0
是一样的。 即使Python对待它们的方式不同,尝试使用1
和1.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
所以我想说,一般来说,你的问题的答案是“这是否是一个有用的语言function?” 是“不,可能不是”。