Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Order of insertion in sets (when parsing {}) [duplicate]

Someone asked here why when putting 1 and True in a set only 1 is kept.

This is of course because 1==True. But in which cases 1 is kept and in which cases True is kept?

Let's see:

passing a list to build the set instead of using the set notation:

>>> set([True,1])
{True}
>>> set([1,True])
{1}

seems logical: set iterates on the inner list, and doesn't add the second element because it is equal to the first element (note that set([True,1]) cannot yield 1, because set cannot know what's inside the list. It may even not be a list but an iterable)

Now using set notation:

>>> {True,1}
{1}
>>> {1,True}
{True} 

It seems that in that case, the list of items is processed in reverse order (tested on Python 2.7 and Python 3.4).

But is that guaranteed? Or just an implementation detail?

like image 511
Jean-François Fabre Avatar asked Sep 01 '17 16:09

Jean-François Fabre


2 Answers

The order the elements in the set literal will be inserted does not seem to be guaranteed by the language specification. However, Python 3.6 was changed so that it has the expected left-to-right evaluation order. For full details of this change, here is the issue, and also the commit that introduced the change in insertion order.


To describe the change in a bit more detail, building the set literal {True, 1} triggers the BUILD_SET opcode (with oparg equal to 2) after first pushing pointers to True and 1 onto the virtual machine's internal stack.

In Python 3.4, BUILD_SET uses the following loop to insert elements into the set (note that oparg is 2 in our case):

while (--oparg >= 0) {
    PyObject *item = POP();
    if (err == 0)
        err = PySet_Add(set, item);
        Py_DECREF(item);

Since 1 was added to the stack last, it is popped off first and is the first object inserted into the set.

In newer versions of Python (such as 3.6), the BUILD_SET opcode uses PEEK instead of POP:

for (i = oparg; i > 0; i--) {
    PyObject *item = PEEK(i);
    if (err == 0)
        err = PySet_Add(set, item);
        Py_DECREF(item);

PEEK(i) fetches the ith item down the stack, so for {True, 1}, the object True is added to the set first.

like image 129
Alex Riley Avatar answered Sep 22 '22 06:09

Alex Riley


Left to right order in the set display is guaranteed by the documentation:

its elements are evaluated from left to right and added to the set object

Example:

>>> def f(i, seq="left to right".split()): print(seq[i])
>>> {f(0), f(1), f(2)}
left
to
right
{None}

Therefore, {1, True} is effectively:

>>> S = set()
>>> S.add(1)
>>> S.add(True) # no effect according to docs
>>> S
{1}

The set may contain only one of True or 1 because they are duplicates from the set point of view:

>>> hash(1) == hash(True)
True
>>> 1 == True
True

It is guaranteed on Python 3 that 1 == True. See Is `False == 0 and True == 1 in Python an implementation detail or is it guaranteed by the language?:

Booleans: These represent the truth values False and True [...] Boolean values behave like the values 0 and 1, respectively, in almost all contexts, the exception being that when converted to a string, the strings "False" or "True" are returned, respectively.

If {1, True} prints {True} then it is a bug ("set_display evaluation order doesn't match documented behaviour"). The output should be the same as set([1, True]). It works as expected ({1}) on the recent pypy, jython, and cpython 2.7.13+, 3.5.3+ versions.

like image 23
jfs Avatar answered Sep 21 '22 06:09

jfs