The Python Oracle

Unexpected output using Pythons' ternary operator in combination with lambda

--------------------------------------------------
Hire the world's top talent on demand or became one of them at Toptal: https://topt.al/25cXVn
and get $2,000 discount on your first invoice
--------------------------------------------------

Music by Eric Matyas
https://www.soundimage.org
Track title: Over a Mysterious Island Looping

--

Chapters
00:00 Unexpected Output Using Pythons' Ternary Operator In Combination With Lambda
01:30 Accepted Answer Score 11
03:21 Thank you

--

Full question
https://stackoverflow.com/questions/3410...

--

Content licensed under CC BY-SA
https://meta.stackexchange.com/help/lice...

--

Tags
#python #boolean #conditionaloperator

#avk47



ACCEPTED ANSWER

Score 11


I think I figured out why the bug is happening, and why your repro is Python 3 specific.

Code objects do equality comparisons by value, rather than by pointer, strangely enough:

static PyObject *
code_richcompare(PyObject *self, PyObject *other, int op)
{
    ...

    co = (PyCodeObject *)self;
    cp = (PyCodeObject *)other;

    eq = PyObject_RichCompareBool(co->co_name, cp->co_name, Py_EQ);
    if (eq <= 0) goto unequal;
    eq = co->co_argcount == cp->co_argcount;
    if (!eq) goto unequal;
    eq = co->co_kwonlyargcount == cp->co_kwonlyargcount;
    if (!eq) goto unequal;
    eq = co->co_nlocals == cp->co_nlocals;
    if (!eq) goto unequal;
    eq = co->co_flags == cp->co_flags;
    if (!eq) goto unequal;
    eq = co->co_firstlineno == cp->co_firstlineno;
    if (!eq) goto unequal;

    ...

In Python 2, lambda e: True does a global name lookup and lambda e: 1 loads a constant 1, so the code objects for these functions don't compare equal. In Python 3, True is a keyword and both lambdas load constants. Since 1 == True, the code objects are sufficiently similar that all the checks in code_richcompare pass, and the code objects compare the same. (One of the checks is for line number, so the bug only appears when the lambdas are on the same line.)

The bytecode compiler calls ADDOP_O(c, LOAD_CONST, (PyObject*)co, consts) to create the LOAD_CONST instruction that loads a lambda's code onto the stack, and ADDOP_O uses a dict to keep track of objects it's added, in an attempt to save space on stuff like duplicate constants. It has some handling to distinguish things like 0.0, 0, and -0.0 that would otherwise compare equal, but it wasn't expected that they'd ever need to handle equal-but-inequivalent code objects. The code objects aren't distinguished properly, and the two lambdas end up sharing a single code object.

By replacing True with 1.0, we can reproduce the bug on Python 2:

>>> f1, f2 = lambda: 1, lambda: 1.0
>>> f2()
1

I don't have Python 3.5, so I can't check whether the bug is still present in that version. I didn't see anything in the bug tracker about the bug, but I could have just missed the report. If the bug is still there and hasn't been reported, it should be reported.