为什么由不同初始化集合构建的元组是相等的?

38

我期望以下两个元组

>>> x = tuple(set([1, "a", "b", "c", "z", "f"]))
>>> y = tuple(set(["a", "b", "c", "z", "f", 1]))

相比较而言,它们并不相等:

>>> x == y
>>> True
为什么这样?

23
你为什么认为它是错误的——这两个集合的内容是相同的! - Tony Suffolk 66
“Relevant.”(相关的)。正如Zero Piraeus所说,哈希随机化使得对于字符串、字节和日期而言这是不正确的,因为每次实例化解释器时,hash(a_string)都会发生变化。 - Veedrac
4个回答

69

乍一看,似乎x应该总是等于y,因为由相同元素构成的两个集合总是相等的:

>>> x = set([1, "a", "b", "c", "z", "f"])
>>> y = set(["a", "b", "c", "z", "f", 1])
>>> x
{1, 'z', 'a', 'b', 'c', 'f'}
>>> y
{1, 'z', 'a', 'b', 'c', 'f'}
>>> x == y
True

然而,由两个相同集合构成的元组(或其他有序集合)并不总是相等的。

实际上,在 Python >= 3.3 中,您比较的结果有时为True,有时为False。测试以下代码:

# compare.py
x = tuple(set([1, "a", "b", "c", "z", "f"]))
y = tuple(set(["a", "b", "c", "z", "f", 1]))
print(x == y)

...一千次:

$ for x in {1..1000}
> do
>   python3.3 compare.py
> done | sort | uniq -c
147 False
853 True

这是因为自 Python 3.3 起,字符串、字节和日期时间的哈希值由于一项安全修复被随机化。取决于哈希值,“冲突”可能会发生,这意味着存储在基础数组中的项目顺序(因此迭代顺序)取决于插入顺序。

以下是文档中相关的部分:

安全性改进:

  • 哈希随机化默认打开。

https://docs.python.org/zh-cn/3/whatsnew/3.3.html

编辑:由于评论中提到上面的True/False比例看起来令人惊讶...

集合和字典一样是使用哈希表实现的 - 因此如果出现冲突,则表中的项目顺序(以及迭代顺序)将取决于哪个项目先添加(在此示例中不同于xy)以及用于哈希的种子(自 Python 3.3 以来,由于不同的Python调用而不同)。由于冲突是有意设计得很少出现的,并且本问题中的示例集合较小,因此这个问题并不会像一开始可能认为的那样经常出现。

有关Python字典和集合实现的详细说明,请参见The Mighty Dictionary


3
正如我所说,自Python 3.3版本以来,行为已经发生了变化。请参见object.__hash__:"更改哈希值会影响字典、集合和其他映射的迭代顺序。 Python从未保证过这种排序(在32位和64位版本之间通常会有所不同)"(我强调了一下)。 - Zero Piraeus
1
哈希值可能仍然完全随机,但853/1000不是碰撞率的衡量标准,而是两个不同集合之间哈希值顺序相同的衡量标准。根据哈希的计算方式,即使在高随机性的情况下(通过对item进行多次哈希调用的平均结果)hash("a")<hash("b")也可能经常为真。 - Tony Suffolk 66
1
@TonySuffolk66 不,它确实是碰撞的度量 - 重要的是底层数组中的顺序,只有在两个项目的(truncated)哈希相同(因此第二个插入的项目必须放在不同于基于其哈希的第一个选择的地方)时,xy之间才会有所不同。 - Zero Piraeus
3
@Floris 数字为5/32。参见:https://dev59.com/hV8e5IYBdhLWcg3wEG_A#26136895 - Veedrac
2
@TonySuffolk66 这正是碰撞率的度量标准。请参见:https://dev59.com/hV8e5IYBdhLWcg3wEG_A#26136895 - Veedrac
显示剩余5条评论

12

这里有两个要点。

  1. 集合是无序的。set([1, "a", "b", "c", "z", "f"])) == set(["a", "b", "c", "z", "f", 1])

  2. 当你通过tuple构造函数将一个集合转换为元组时,它本质上是遍历该集合并添加迭代返回的每个元素。

元组的构造函数语法为

tuple(iterable) -> tuple initialized from iterable's items

调用 tuple(set([1, "a", "b", "c", "z", "f"])) 和调用 tuple([i for i in set([1, "a", "b", "c", "z", "f"])]) 是相同的。

值为

[i for i in set([1, "a", "b", "c", "z", "f"])]

[i for i in set(["a", "b", "c", "z", "f", 1])]

迭代器在遍历相同的集合时与其相同。

编辑 感谢@ZeroPiraeus(查看他的答案)。这并不是保证的。即使对于相同的集合,迭代的值也不一定相同。

元组构造函数不知道集合的构造顺序。


1
那么,如果两个 set 中的值相同,它们是否保证以相同的顺序进行迭代? - stalk
3
不,不是的:请参见我的回答 - Zero Piraeus
2
你应该根据@ZeroPiraeus的回答更新你的答案 - “_和_的值与它迭代相同的集合不总是相同的”。 - icedtrees

6

集合不是有序的,只能通过它们的成员来定义。

例如,set([1, 2]) == set([2, 1])

如果元组的每个位置上的成员都相等,则元组相等,但由于创建元组的集合以相同的方式迭代(按递增顺序),所以元组最终也相等。


5

你有两个列表 - 它们具有相同的内容但顺序不同,你将它们转换为集合 - 这些集合将相等,因为它们具有相同的内容。

当你将这些集合转换为元组时,它们将按相同的顺序转换,因为它们是相同的集合,所以元组将是相同的。

在Python2.7中是正确的 - 但从3.3开始,哈希值被随机化后,你不能保证这一点 - 因为两个集合虽然内容相等,但迭代顺序不一定相同。


我没有给你的回答点踩,但是无论哈希随机化如何,你的回答都是不正确的:例如,在每个 Python 版本中,自从 2.4 版本开始(当 set 成为内置函数时),tuple(set([0, 8])) != tuple(set([8, 0]))(在 Python 2.3 中也适用于 sets.Set)。 - Zero Piraeus
所以一个集合并没有做它应该做的事情 - 即当内容相等时应该是相等的 - 这非常奇怪。 - Tony Suffolk 66
两个对象相等并不一定意味着对它们进行同样的操作会得到相同的结果。在这种情况下,结果发生的原因是使用了具有不同迭代顺序的两个相等集合来构建一个关注顺序的对象,但还有其他例子,例如 8.0 == 8,但是 str(8.0) != str(8) - Zero Piraeus

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接