迭代集合的顺序在每次运行时都会发生变化。

41
为什么Python集合(使用相同内容)的迭代顺序在每次运行中都会变化?有哪些选项可以使其在每次运行中保持一致?
我了解Python集合的迭代顺序是任意的。如果我将'a','b'和'c'放入一个集合中,然后对它们进行迭代,它们可能以任何顺序返回。
我观察到的是,在程序运行期间,这个顺序保持不变。也就是说,如果我的程序连续两次迭代同一个集合,我两次都得到相同的顺序。但是,如果我连续运行程序两次,顺序会随着每次运行而改变。
不幸的是,这破坏了我的自动化测试之一,该测试只是比较程序两次运行的输出。我并不关心实际的顺序,但我希望它在每次运行中保持一致。
我想到的最好的解决方案是:
  1. 将集合复制到列表中。
  2. 对列表应用任意排序。
  3. 遍历列表而不是集合。
是否有更简单的解决方案?
注意:我在StackOverlow上找到了类似的问题,但没有解决这个特定问题,即如何在每次运行中获得相同的结果。

如果你要测试的是“程序两次输出相同”,那么排序列表选项是最好的选择。如果你要测试的是“程序两次创建相同的集合”,你需要进行集合比较(通过将两次运行的输出进行pickle,然后解pickle这两个输出并进行集合比较,或者进行类似的操作)。 - Russell Borogove
@Russell:我有单元测试来验证集合内容。但我还有一个测试,将两次运行的输出进行比较以进行检查。输出在某种程度上取决于集合中项目的顺序,但只是迂回地影响。 - Adrian McCarthy
即使Python的字典按插入顺序排序,但是"Set iteration order is still variable"(集合迭代顺序仍然是可变的)。 - Josiah Yoder
7个回答

34

集合迭代顺序在每次运行时变化的原因似乎是因为Python默认使用哈希种子随机化。(详见命令选项-R。) 因此,集合迭代不仅是任意的(由于哈希),而且是非确定性的(由于随机种子)。

您可以通过为解释器设置环境变量PYTHONHASHSEED的固定值来覆盖随机种子。 每次运行使用相同的种子意味着集合迭代仍然是任意的,但现在它是确定性的,这是期望的属性。

哈希种子随机化是一项安全措施,用于使对手很难提供会导致病态行为的输入(例如创建大量哈希冲突)。 对于单元测试而言,这并不是一个问题,因此在运行测试时覆盖哈希种子是合理的。


Python 直到 2012 年才添加了随机哈希功能。 - pydsigner
@pydsigner:这很有趣,因为它确实解决了我面临的问题。去年秋天我回到了这个项目,设置PYTHONHASHSEED使得我的测试输出在每次运行时都是一致的。 - Adrian McCarthy
确实很有趣...这是在2.6.83.2.3版本中引入的。 - pydsigner
@pydsigner,你是否也有随机化介绍的链接(不是PYTHONHASHSEED变量的介绍)? - superb rain
1
根据http://ocert.org/advisories/ocert-2011-003.html,@superbrain看起来版本与PYTHONHASHSEED的添加相一致;已回溯到2.6.8和3.1.5,并且在2.7.3和3.2.3之外的所有地方都可用。 - pydsigner
@pydsigner 谢谢。我曾尝试在代码历史记录中找到它,但失败了。所以我猜当时提问的确有其他原因。嗯,算了吧... - superb rain

17

使用对称差(^)运算符在两个集合上,以查看是否存在任何差异:

In [1]: s1 = set([5,7,8,2,1,9,0])
In [2]: s2 = set([9,0,5,1,8,2,7])
In [3]: s1
Out[3]: set([0, 1, 2, 5, 7, 8, 9])
In [4]: s2
Out[4]: set([0, 1, 2, 5, 7, 8, 9])
In [5]: s1 ^ s2
Out[5]: set()

这对于直接比较集合来说是可以的。然而,在我的测试中,我正在寻找一种简单的方法来比较一个运行的输出和另一个运行的输出,而且这个输出受迭代顺序的影响。 - Adrian McCarthy

14

您想要的不可能。"任意"的意思就是任意。

我的解决方案和您的一样,如果您想将其与另一个进行比较,则必须对集合进行排序。


17
我猜我以为“随意”的意思是取决于内容,而不是月相。 - Adrian McCarthy
1
嗯,有任意的,然后有非确定性的。你可能可以确定集合中的顺序,但我敢打赌这会带来更多麻烦。在Python中检查有序集或类似的东西... - JoshD
6
即使其在每次运行中都保持一致,也无法保证在不同机器、不同 Python 版本以及 CPython 与 Jython 等环境下一致。 - Mike Axiak
3
即使在同一台机器上相同的 Python 版本中,'相同的内容' 也不是保证。项目是基于哈希值插入的。当多个项目具有相同的哈希值时,它们会根据插入顺序插入到不同的位置。删除项目可能会导致更多不同的排序方式。还有一些项目的哈希值取决于它们的内存位置,这使得它们在运行之间不同。除了使用 sorted() 方便地编写三个步骤外,没有太多可以做的。 - Thomas Wouters
1
不确定,但我猜想某些时候会通过地址(即id())进行哈希处理,系统中的某些异步因素会使内存管理器在不同的运行中发生扰动。我不认为cpython在哈希处理中涉及PRNG。 - Russell Borogove
显示剩余3条评论

6
该集合的迭代顺序不仅取决于其内容,还取决于将项插入集合的顺序以及是否在此过程中进行了删除。因此,您可以使用不同的插入和删除创建两个不同的集合,并最终得到相同的集合,但具有不同的迭代顺序。
正如其他人所说:如果您关心集合的顺序,则必须从中创建一个排序列表。

1
连续两次使用相同输入运行我的程序,涉及相同的插入、删除和设置操作序列,但迭代顺序仍然会改变。就好像还有其他因素参与其中,比如时间、进程 ID 或其他随机因素在每次运行时都不同。 - Adrian McCarthy
4
Thomas Wouters在上面的评论中指出,某些类在哈希函数中使用id(),这意味着对象的哈希值取决于其内存地址,而且谁知道会发生什么使得它们不同。如果你正在使用自己的类,你可以编写自己的__hash__函数来消除一些这种不确定性,但最好还是简单地排序结果。 - Ned Batchelder

5
您的问题被转化成了两个问题:A)如何在特定情况下比较“两次运行的输出”;B)集合中的迭代顺序定义是什么。如果适当的话,您可能应该将它们区分开来并将B)作为一个新问题发布。我会回答A。
在我看来,在您的情况下使用排序列表不是一个非常干净的解决方案。您应该决定是否一劳永逸地关心迭代顺序,并使用适当的结构。
要么1)您想要比较两个集合以查看它们是否具有相等的内容,而与顺序无关。然后,集合上的简单==运算符似乎是合适的。请参见python2 setspython3 sets
要么2)您想要检查元素是否以相同的顺序插入。但这似乎只有在插入顺序对您的库的用户有所影响时才是合理的,在这种情况下,使用集合类型可能是不合适的。换句话说,“比较两次运行的输出”是什么意思,以及为什么要这样做还不清楚。
在所有情况下,我都认为在这里使用排序列表是不合适的。

1
你可以将预期结果设置为一个集合。使用 == 检查这两个集合是否相等。

-1

与集合不同,列表始终具有保证的顺序,因此您可以放弃集合并使用列表。


是的,但我正在使用多个集合操作(并集、交集等)创建集合。它们比列表效率低。 - Adrian McCarthy

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