为什么字典排序是非确定性的?

49 投票
2 回答
13861 浏览
提问于 2025-04-17 16:21

我最近从Python 2.7切换到了Python 3.3,发现一个有趣的现象。在Python 2中,字典的键的顺序是随机的,但每次运行都是一致的。而在Python 3中,通过比如说vars()获取的字典键的顺序似乎变得不确定了。

如果我在Python 2.7和Python 3.3中运行:

class Test(object): pass
parameters = vars(Test)
print(list(parameters.keys()))

那么:

  • 在Python 2.7中,我总是能得到

    ['__dict__', '__module__', '__weakref__', '__doc__']
    
  • 而在Python 3.3中,我可能会得到任何随机的顺序,比如:

    ['__weakref__', '__module__', '__qualname__', '__doc__', '__dict__']
    ['__doc__', '__dict__', '__qualname__', '__module__', '__weakref__']
    ['__dict__', '__module__', '__qualname__', '__weakref__', '__doc__']
    ['__weakref__', '__doc__', '__qualname__', '__dict__', '__module__']
    

这种不确定性是从哪里来的呢?而像

list({str(i): i for i in range(10)}.keys())

这样的东西为什么在每次运行时都是一致的,总是给出

['3', '2', '1', '0', '7', '6', '5', '4', '9', '8']

… 呢?

2 个回答

14

请注意,Python 3.7 中的集合(sets)仍然是非确定性的,也就是说它们的顺序不是固定的。虽然字典(dicts)会保持你添加元素的顺序,但集合却不会。集合的表现可能会有随机性。

python3 -c "print({str(i) for i in range(9)})"

每次运行这个代码时,得到的结果可能都会不同。

63

更新:在Python 3.6中,dict(字典)有了一个新的实现,它可以保持插入的顺序。从Python 3.7开始,这种保持顺序的特性是有保障的:

字典对象保持插入顺序的特性已经被正式声明为Python语言规范的一部分。


这项变化源于2012年的一个安全修复,这个修复在Python 3.3中默认启用(可以往下滚动到“安全改进”部分)。

在公告中提到:

哈希随机化导致字典和集合的迭代顺序变得不可预测,并且在不同的Python运行中可能会有所不同。Python从未保证字典或集合中键的迭代顺序,建议应用程序不要依赖于此。历史上,字典的迭代顺序在不同版本之间变化不大,并且在连续执行Python时始终保持一致。因此,一些现有的应用程序可能依赖于字典或集合的顺序。由于这个原因,以及许多不接受不可信输入的Python应用程序不容易受到这种攻击的影响,在这里提到的所有稳定的Python版本中,哈希随机化默认是禁用的。

如上所述,最后那句大写的内容在Python 3.3中已经不再成立。

另见:有关object.__hash__()的文档(“注意”侧边栏)。

如果确实需要,你可以通过将环境变量PYTHONHASHSEED设置为0来禁用受此行为影响的Python版本中的哈希随机化。


你的反例:

list({str(i): i for i in range(10)}.keys())

… 实际上在Python 3.3中并不总是给出相同的结果,尽管不同的排序方式数量有限,这与哈希碰撞的处理方式有关:

$ for x in {0..999}
> do
>   python3.3 -c "print(list({str(i): i for i in range(10)}.keys()))"
> done | sort | uniq -c
     61 ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']
     73 ['1', '0', '3', '2', '5', '4', '7', '6', '9', '8']
     62 ['2', '3', '0', '1', '6', '7', '4', '5', '8', '9']
     59 ['3', '2', '1', '0', '7', '6', '5', '4', '9', '8']
     58 ['4', '5', '6', '7', '0', '1', '2', '3', '8', '9']
     55 ['5', '4', '7', '6', '1', '0', '3', '2', '9', '8']
     62 ['6', '7', '4', '5', '2', '3', '0', '1', '8', '9']
     63 ['7', '6', '5', '4', '3', '2', '1', '0', '9', '8']
     60 ['8', '9', '0', '1', '2', '3', '4', '5', '6', '7']
     66 ['8', '9', '2', '3', '0', '1', '6', '7', '4', '5']
     65 ['8', '9', '4', '5', '6', '7', '0', '1', '2', '3']
     53 ['8', '9', '6', '7', '4', '5', '2', '3', '0', '1']
     62 ['9', '8', '1', '0', '3', '2', '5', '4', '7', '6']
     52 ['9', '8', '3', '2', '1', '0', '7', '6', '5', '4']
     73 ['9', '8', '5', '4', '7', '6', '1', '0', '3', '2']
     76 ['9', '8', '7', '6', '5', '4', '3', '2', '1', '0']

正如本答案开头所提到的,在Python 3.6中情况已经不一样了:

$ for x in {0..999}
> do
>   python3.6 -c "print(list({str(i): i for i in range(10)}.keys()))"
> done | sort | uniq -c
   1000 ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']

撰写回答