18

Containers that take hashable objects (such as dict keys or set items). As such, a dictionary can only have one key with the value 1, 1.0 or True etc. (note: simplified somewhat - hash collisions are permitted, but these values are considered equal)

My question is: is the parsing order well-defined and is the resulting object predictable across implementations? For example, OSX Python 2.7.11 and 3.5.1 interprets dict like so:

>>> { True: 'a', 1: 'b', 1.0: 'c', (1+0j): 'd' }
{True: 'd'}

In this case, it appears that the first key and the last value are preserved.

Similar, in the case of set:

>>> { True, 1, 1.0, (1+0j) }
set([(1+0j)])

Here it appears that the last item is preserved.

But (as mentioned in comments):

>>> set([True, 1, 1.0])
set([True])

Now the first in the iterable is preserved.

The documentation notes that the order of items (for example in dict.items) is undefined, however my question refers to the result of constructing dict or set objects.

styvane
  • 59,869
  • 19
  • 150
  • 156
Hamish
  • 22,860
  • 8
  • 53
  • 67
  • 2
    But note that `set([ True, 1, 1.0, (1+0j) ])` gives `set([True])` – Tom Karzes Jan 06 '16 at 00:57
  • Are you strictly talking about dict and set literals? – Padraic Cunningham Jan 06 '16 at 01:05
  • @TomKarzes Ah, that's interesting. Will add above. – Hamish Jan 06 '16 at 01:07
  • @PadraicCunningham not strictly, no. More to the point: is this behaviour defined and consistent. – Hamish Jan 06 '16 at 01:09
  • @Padraic that documentation says the set literal is "evaluated left to right and added to the set object". But what we have seen implies the exact opposite: They are added right to left for sets using `{}` notation. Remember, the first value added is the one that is retained. – Tom Karzes Jan 06 '16 at 01:55
  • keeping the first key and the last value, when they didn't even match, is super weird – wim Jan 06 '16 at 09:46
  • @wim no, that's actually understandable (the numeric keys are equivalent, only the value is updated after the first element is added), and matches the documented behaviour :D – Hamish Jan 06 '16 at 20:23

1 Answers1

8
  • The bug is now fixed in recent versions of python as explained in @jsf's answer

dictionary-displays

If a comma-separated sequence of key/datum pairs is given, they are evaluated from left to right to define the entries of the dictionary: each key object is used as a key into the dictionary to store the corresponding datum. This means that you can specify the same key multiple times in the key/datum list, and the final dictionary’s value for that key will be the last one given.

A dict comprehension, in contrast to list and set comprehensions, needs two expressions separated with a colon followed by the usual “for” and “if” clauses. When the comprehension is run, the resulting key and value elements are inserted in the new dictionary in the order they are produced.

set displays

A set display yields a new mutable set object, the contents being specified by either a sequence of expressions or a comprehension. When a comma-separated list of expressions is supplied, its elements are evaluated from left to right and added to the set object. When a comprehension is supplied, the set is constructed from the elements resulting from the comprehension.

There is a difference in calling the set constructor or using a comprehension and the plain literal.

def f1():
    return {x for x in [True, 1]}

def f2():
    return set([True, 1])
def f3():
    return {True, 1}
print(f1())
print(f2())
print(f3())
import dis

print("f1")
dis.dis(f1)

print("f2")

dis.dis(f2)

print("f3")
dis.dis(f3)

Output:

{True}
{True}
{1}

How they are created influences the outcome:

    605           0 LOAD_CONST               1 (<code object <setcomp> at 0x7fd17dc9a270, file "/home/padraic/Dropbox/python/test.py", line 605>)
              3 LOAD_CONST               2 ('f1.<locals>.<setcomp>')
              6 MAKE_FUNCTION            0
              9 LOAD_CONST               3 (True)
             12 LOAD_CONST               4 (1)
             15 BUILD_LIST               2
             18 GET_ITER
             19 CALL_FUNCTION            1 (1 positional, 0 keyword pair)
             22 RETURN_VALUE
f2
608           0 LOAD_GLOBAL              0 (set)
              3 LOAD_CONST               1 (True)
              6 LOAD_CONST               2 (1)
              9 BUILD_LIST               2
             12 CALL_FUNCTION            1 (1 positional, 0 keyword pair)
             15 RETURN_VALUE
f3
611           0 LOAD_CONST               1 (True)
              3 LOAD_CONST               2 (1)
              6 BUILD_SET                2
              9 RETURN_VALUE

Python only runs the BUILD_SET bytecode when you pass a pure literal separated by commas as per:

When a comma-separated list of expressions is supplied, its elements are evaluated from left to right and added to the set object.

The line for the comprehension:

When a comprehension is supplied, the set is constructed from the elements resulting from the comprehension.

So thanks to Hamish filing a bug report it does indeed come down to the BUILD_SET opcode as per Raymond Hettinger's comment in the link The culprit is the BUILD_SET opcode in Python/ceval.c which unnecessarily loops backwards, the implementation of which is below:

 TARGET(BUILD_SET) {
            PyObject *set = PySet_New(NULL);
            int err = 0;
            if (set == NULL)
                goto error;
            while (--oparg >= 0) {
                PyObject *item = POP();
                if (err == 0)
                    err = PySet_Add(set, item);
                Py_DECREF(item);
            }
            if (err != 0) {
                Py_DECREF(set);
                goto error;
            }
            PUSH(set);
            DISPATCH();
        }
Padraic Cunningham
  • 176,452
  • 29
  • 245
  • 321
  • 1
    Yep, the case with `f3` certainly appears to be unexpected. Note the info here: http://bugs.python.org/issue26020 - it's all a bit academic, it does not _matter_ in any real situation, but it's certainly interesting. Thank you for the detailed answer! – Hamish Jan 06 '16 at 20:18
  • @Hamish, cool, I looked for the BUILD_SET implementation last night but I could not stay awake long enough to find it, I will update the answer, thanks for letting me know. – Padraic Cunningham Jan 06 '16 at 20:31
  • could you highlight a little bit more that the difference in behavior is a *bug* It is fixed now. Python guarantees the same answer for f1, f2, f3: `def f4(): S = set(); S.add(True); S.add(1); return S` See [Order of insertion in sets (when parsing {})](https://stackoverflow.com/a/46007408/4279). – jfs Sep 02 '17 at 06:27
  • @jfs, sure, I added a note and link to your answer. – Padraic Cunningham Sep 03 '17 at 08:36