Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Set literal gives different result from set function call

Why does the set function call wipe out the dupes, but parsing a set literal does not?

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

(Python 2.7.12. Possibly same root cause as for this similar question)

like image 315
wim Avatar asked Oct 24 '16 19:10

wim


2 Answers

Sets test for equality, and until there are new Python releases, the order in which they do this can differ based on the form you hand the values to the set being constructed, as I'll show below.

Since 0 == x is true and 0 == y is true, but x == y is false, the behaviour here is really undefined, as the set assumes that x == y must be true if the first two tests were true too.

If you reverse the list passed to set(), then you get the same output as using a literal, because the order of equality tests changes:

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

and the same for reversing the literal:

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

What's happening is that the set literal loads the values onto the stack and then the stack values are added to the new set object in reverse order.

As long as 0 is loaded first, the other two objects are then tested against 0 already in the set. The moment one of the other two objects is loaded first, the equality test fails and you get two objects added:

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

That set literals add elements in reverse is a bug present in all versions of Python that support the syntax, all the way until Python 2.7.12 and 3.5.2. It was recently fixed, see 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 reads the stack from the top down:

# 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); } 

while the bytecode adds elements to the stack in reverse order (pushing 0 on the stack first):

>>> 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 

The fix is to read the elements from the stack in reverse order; the Python 2.7.13 version uses PEEK() instead of POP() (and a STACKADJ() to remove the elements from the stack afterwards):

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

The equality testing issue has the same root cause as the other question; the Decimal() class is having some equality issues with complex here, which was fixed in Python 3.2 (by making Decimal() support comparisons to complex and a few other numeric types it didn't support before).

like image 87
Martijn Pieters Avatar answered Sep 19 '22 21:09

Martijn Pieters


It's all down to the order in which the set is constructed, combined with the bug you discovered with your other question. It appears that the literal is constructed in the opposite order to conversion from a list.

>>> {0, x, y} set([0j, Decimal('0')]) >>> {y, x, 0} set([0]) 
like image 40
Mark Ransom Avatar answered Sep 20 '22 21:09

Mark Ransom