Set literal 给出与 set 函数调用不同的结果

Set literal gives different result from set function call

为什么 set 函数调用消除了欺骗,但解析集合文字却没有?

>>> x = Decimal('0')
>>> y = complex(0,0)
>>> set([0, x, y])
{0}
>>> {0, x, y}
{Decimal('0'), 0j}

(Python 2.7.12。可能与 类似问题的根本原因相同)

设置相等性测试,直到有新的 Python 版本,它们执行此操作的顺序可能会根据您将值传递给正在构建的集合的形式而有所不同,正如我将展示的那样下面。

因为0 == x为真并且0 == y为真,但是x == y ,这里的行为实际上是 undefined,因为集合假定如果前两个测试也为真,则 x == y 必须为真。

如果你反转传递给set()的列表,那么你会得到与使用文字相同的输出,因为相等测试的顺序改变了:

>>> set([y, x, 0])
set([0j, Decimal('0')])

与反转文字相同:

>>> {y, x, 0}
set([0])

发生的事情是集合 literal 将值加载到堆栈上,然后堆栈值以相反的顺序添加到新的集合对象中。

只要0首先被加载,其他两个对象就会针对集合中已经存在的0进行测试。首先加载其他两个对象之一的那一刻,相等性测试失败并且您添加了两个对象:

>>> {y, 0, x}
set([Decimal('0'), 0j])
>>> {x, 0, y}
set([0j, Decimal('0')])

set literals add elements in reverse 是所有支持语法的 Python 版本中存在的错误,一直到 Python 2.7.12 和 3.5.2。它最近已修复,参见 issue 26020 (part of 2.7.13, 3.5.3 and 3.6, none of which have been released yet). If you look at 2.7.12, you can see that BUILD_SET in ceval.c 从上到下读取堆栈:

# oparg is the number of elements to take from the stack to add
for (; --oparg >= 0;) {
    w = POP();
    if (err == 0)
        err = PySet_Add(x, w);
    Py_DECREF(w);
}

而字节码以相反的顺序将元素添加到堆栈(先将 0 压入堆栈):

>>> from dis import dis
>>> dis(compile('{0, x, y}', '', 'eval'))
  2           0 LOAD_CONST               1 (0)
              3 LOAD_GLOBAL              0 (x)
              6 LOAD_GLOBAL              1 (y)
              9 BUILD_SET                3
             12 RETURN_VALUE

修复方法是以相反的顺序从堆栈中读取元素; Python 2.7.13 version 使用 PEEK() 而不是 POP() (然后使用 STACKADJ() 从堆栈中删除元素):

for (i = oparg; i > 0; i--) {
    w = PEEK(i);
    if (err == 0)
        err = PySet_Add(x, w);
    Py_DECREF(w);
}
STACKADJ(-oparg);

相等性测试问题与另一个问题的根本原因相同; Decimal() class 在此处与 complex 存在一些相等性问题,已在 Python 3.2 中修复(通过制作 Decimal() support comparisons to complex and a few other numeric types it didn't support before)。

这完全取决于集合的构建顺序,再加上您在 中发现的错误。看起来文字的构造顺序与从列表转换的顺序相反。

>>> {0, x, y}
set([0j, Decimal('0')])
>>> {y, x, 0}
set([0])