【问题标题】:Is python's "set" stable?python的“set”稳定吗?
【发布时间】:2022-12-22 16:08:18
【问题描述】:

在回答另一个 SO 问题 (there) 时出现了这个问题。

当我在 python 集上迭代几次(在调用之间不更改它)时,我可以假设它总是以相同的顺序返回元素吗?如果不是,更改顺序的理由是什么?它是确定性的还是随机的?或实现定义?

当我重复调用同一个 python 程序时(不是随机的,不依赖于输入),我会得到相同的集合顺序吗?

潜在的问题是 python 集合迭代顺序是否仅取决于用于实现集合的算法,或者还取决于执行上下文?

【问题讨论】:

  • 我认为这里的共识是,没有任何理智的语言会提供一种顺序自发改变的数据结构,但没有一个理智的程序员会在没有被明确告知的情况下做出这样的假设。清楚地答案是默认不变性。
  • @JoshLee:Go 的 map 迭代是故意随机化的,以捕获由排序假设引起的错误。
  • 我会使用术语“确定性”而不是“稳定”

标签: python set


【解决方案1】:

没有关于集合稳定性的正式保证。然而,在 CPython 实现中,只要不改变集合,项目就会以相同的顺序生成。集合被实现为开放寻址哈希表(带有素数探针),因此插入或删除项目可以完全改变顺序(特别是,当这触发调整大小时,它会重新组织项目在内存中的布局方式。)您还可以有两个相同的集合,但它们以不同的顺序生成项目,例如:

>>> s1 = {-1, -2}
>>> s2 = {-2, -1}
>>> s1 == s2
True
>>> list(s1), list(s2)
([-1, -2], [-2, -1])

除非你非常确定你有相同的集合并且在两次迭代之间没有任何东西触及它,否则最好不要依赖它保持不变。对您调用的函数进行看似无关的更改可能会产生很难发现的错误。

【讨论】:

  • 我会说至少dict的稳定性是有保证的。文档说:“如果在不对字典进行干预修改的情况下调用 items()、keys()、values()、iteritems()、iterkeys() 和 itervalues(),列表将直接对应。”这意味着如果没有修改字典,重复调用任何这些方法将返回相同的序列。它还说 iter(dict) 是 dict.iterkeys() 的快捷方式
  • 我说不正式的保证”。dict 文档可以更改(这些细节在过去确实发生了变化,更不用说实现之间的差异了);docs.python.org/ref 的“正式”(但相当简洁)语言规范没有提及任何一种方式。
  • @ThomasWouters:语言规范与类型无关,除非它们影响语法并提及碰巧实现高级概念的内置函数。 dict 的文档被认为具有约束力;当然,随着时间的推移,实现发生了变化,但是dict 的文档保证很少。他们特别提到当它是 CPython 实现细节时,迭代排序的可重复性(没有干预修改)不是实现细节。 Python 标准库是规范的,而不仅仅是 Python 语言参考。
  • 作为参考,the dict requirements for keys/items/values since as early as 2.0 mention this repeatability guarantee(见脚注 2)。 set 从来没有做过这样的保证(它在许多版本中与dict 共享算法,但它不是 100% 一致,并且保证不如 dict 有用,所以几乎没有受益于做出保证并对其进行绑定实现。
【解决方案2】:

setfrozenset 本质上是无序集合。在内部,集合基于hash table,键的顺序取决于插入顺序和hash算法。在 CPython(又名标准 Python)中,小于机器字大小(32 位或 64 位)的整数散列为自身,但文本字符串、bytes 字符串和 datetime 对象散列为随机变化的整数;您可以通过设置 PYTHONHASHSEED 环境变量来控制它。

来自 __hash__ 文档:

笔记

默认情况下,strbytesdatetime__hash__()值 对象被“腌制”成不可预测的随机值。虽然他们 在单个 Python 进程中保持不变,它们不是 在重复调用 Python 之间可预测。

这是为了防止拒绝服务 由利用最坏情况的精心选择的输入引起 字典插入的性能,O(n^2) 复杂度。看 http://www.ocert.org/advisories/ocert-2011-003.html了解详情。

更改哈希值会影响字典、集合和 其他映射。 Python 从未对此顺序做出保证 (它通常在 32 位和 64 位版本之间变化)。

另见 PYTHONHASHSEED。

其他类的散列对象的结果取决于该类的__hash__方法的细节。

所有这一切的结果是,您可以拥有两个包含相同字符串的集合,但是当您将它们转换为列表时,它们可以比较不相等。或者他们可能不会。 ;) 下面是一些演示这一点的代码。在某些运行中,它只是循环,不打印任何东西,但在其他运行中,它会快速找到一个使用与原始顺序不同的集合。

from random import seed, shuffle

seed(42)

data = list('abcdefgh')
a = frozenset(data)
la = list(a)
print(''.join(la), a)

while True:
    shuffle(data)
    lb = list(frozenset(data))
    if lb != la:
        print(''.join(data), ''.join(lb))
        break    

典型输出

dachbgef frozenset({'d', 'a', 'c', 'h', 'b', 'g', 'e', 'f'})
deghcfab dahcbgef

【讨论】:

    【解决方案3】:

    当我调用同一个 python 时 重复编程(不是随机的,不是 输入依赖),我会得到相同的 订购套装?

    经过快速实验后,我现在可以回答这部分问题。使用以下代码:

    class Foo(object) :
      def __init__(self,val) :
        self.val = val
      def __repr__(self) :
        return str(self.val)
    
    x = set()
    for y in range(500) :
      x.add(Foo(y))
    print list(x)[-10:]
    

    我可以触发我在另一个问题中询问的行为。如果我重复运行它,那么输出会改变,但不是每次运行都会改变。它似乎是“弱随机”,因为它变化缓慢。这当然取决于实现,所以我应该说我在雪豹上运行 macports Python2.6。虽然程序会在很长一段时间内输出相同的答案,但做一些影响系统熵池的事情(写入磁盘大部分是有效的)有时会把它踢到不同的输出中。

    Foo 类只是一个简单的 int 包装器,因为实验表明这不会发生在 int 集上。我认为问题是由于该对象缺少 __eq____hash__ 成员引起的,尽管我非常想知道潜在的解释/避免它的方法。同样有用的是一些重现/重复“坏”运行的方法。有谁知道它使用什么种子,或者我如何设置该种子?

    【讨论】:

    • 这非常容易解释:由于缺少 __eq____hash__,您的对象散列基于 id(),并且对象的 ID 在运行之间发生变化。您没有重复打印 list()[-10:]相同的集,只是一个以相同方式创建的。
    • 为什么默认实现当量散列依赖于随机模块......好像他们使用 id() + 别的东西。如果我显式使用 id() 的方法,则行为会发生变化。
    • 他们根本不依赖random 模块。他们只使用对象的id。对象的id是内存地址,所以运行之间由于各种原因,它会有所不同。如果我根据 id() 实施 __hash____eq__,行为是相同的。我不确定你做了什么,但我想你做了一些不同的事情;你必须把代码告诉。或者,您可以不再担心这种行为运行之间,因为无论如何在这种情况下都无法保证集合或命令的顺序。
    • 感谢 cmets。我将总结一些测试代码并提出一个单独的问题。
    【解决方案4】:

    它绝对是实现定义的。 specification of a set 只说了

    作为无序集合,集合不记录元素位置或插入顺序。

    为什么不使用OrderedDict 来创建您自己的 OrderedSet 类?

    【讨论】:

    • 我并不是说我会使用这种行为,只是想知道另一个发帖者看到的错误可能来自哪里。 dict 也有一个非常相似的属性由 python 文档保证(请参阅stackoverflow.com/questions/3666237/…)。为什么 sets 和 dict 之间会有这样的差异是相当令人惊讶的。
    【解决方案5】:

    正如所指出的,这完全是一个实现细节。

    只要你不改变调用之间的结构,应该有没理由对于随时间变化的只读操作(=迭代):没有理智的实现会那样做。即使是可用于实现集合的随机(= 非确定性)数据结构(例如跳跃列表)也不会在没有发生变化时改变读取顺序。

    因此,作为理性的人,您可以放心地依赖这种行为。

    (我知道某些 GC 可能会在后台线程中重新排序内存,但即使这种重新排序在数据结构级别上也不会引人注意,除非出现错误。)

    【讨论】:

    • 出于理性考虑,我们还会尝试在单元测试中捕捉这个假设,这样程序就不会在以后以神秘的方式崩溃。 :)
    • @jleedev:是的,但不幸的是,我可以很容易地看到这样的单元测试无法标记错误:如果行为确实是不确定的,那么为此行为编写可靠的单元测试将非常困难。例如,我在一个并行程序上有一个单元测试套件,由于竞争条件,它在一百次调用中只会失败一次。在 99% 的情况下,它会运行,即使它是一个非常彻底的测试套件。
    【解决方案6】:

    答案很简单不。

    Python 集合操作不稳定。

    我做了一个简单的实验来证明这一点。

    代码:

    import random
    random.seed(1)
    
    x=[]
    class aaa(object):
        def __init__(self,a,b):
            self.a=a
            self.b=b
    
    for i in range(5):
        x.append(aaa(random.choice('asf'),random.randint(1,4000)))
    
    for j in x:
        print(j.a,j.b)
    
    print('====')
    for j in set(x):
        print(j.a,j.b)
    

    运行两次,你会得到这个:

    第一次结果:

    a 2332
    a 1045
    a 2030
    s 1935
    f 1555
    ====
    a 2030
    a 2332
    f 1555
    a 1045
    s 1935
    
    Process finished with exit code 0
    

    第二次结果:

    a 2332
    a 1045
    a 2030
    s 1935
    f 1555
    ====
    s 1935
    a 2332
    a 1045
    f 1555
    a 2030
    
    Process finished with exit code 0
    

    原因在this answer的cmets中有说明。

    但是,有一些方法可以使其稳定:

    【讨论】:

    • 是的,但这不是我问的。您的回答是关于运行相同的进程两次,我的问题是关于在同一进程中对同一组进行两次迭代。好的,这是我问题的后半部分。
    【解决方案7】:

    集合的定义是无序的、唯一的元素 ("Unordered collections of unique elements")。你应该只关心接口,而不是实现。如果你想要一个有序的枚举,你应该把它放入一个列表中并对其进行排序。

    Python 有许多不同的实现。不要依赖未记录的行为,因为您的代码可能会在不同的 Python 实现上中断。

    【讨论】:

      猜你喜欢
      • 2019-08-06
      • 1970-01-01
      • 2010-11-04
      • 1970-01-01
      • 2011-03-07
      • 1970-01-01
      • 1970-01-01
      • 2010-10-11
      • 2012-07-14
      相关资源
      最近更新 更多