Chained conditional expressions in python
Posted on July, 12 2015NOTE: Before you start reading this, read up on stack machines if you have not done so yet. In this post I'll be going over some python bytecode, knowing what a stack machine will help a lot!
False == False in [False]
A friend had recently showed this statement to me, and asked what it evaluates
to. I replied 'False', of course, since False == False
is True
, and True
is not in the list [False]
. Until, of course, I threw it in the python
interpreter.
>>> False == False in [False]
True
Now, now... what's this madness? Is python pulling a javascript? To answer this problem we will disassemble a python function into python bytecode and evaluate it line-by-line.
>>> def func():
... # We'll name these constants as they pop up in the stack
... # A B C
... return False == False in [False]
...
>>> import dis
>>> dis.dis(func)
2 0 LOAD_GLOBAL 0 (False)
3 LOAD_GLOBAL 0 (False)
6 DUP_TOP
7 ROT_THREE
8 COMPARE_OP 2 (==)
11 JUMP_IF_FALSE_OR_POP 24
14 LOAD_GLOBAL 0 (False)
17 BUILD_LIST 1
20 COMPARE_OP 6 (in)
23 RETURN_VALUE
>> 24 ROT_TWO
25 POP_TOP
26 RETURN_VALUE
Lines 0-3
On lines 0-3, LOAD_GLOBAL
is called twice to load A
and B
into the stack. Below is a representation of the stack, with the label/ID mnemonic above each stack element.
# TOS (top-of-stack), TOS1
# B A
stack = [False, False]
Line 6
DUP_TOP
duplicates the 'top' of the stack.
# TOS , TOS1
# DUP.B B A
stack = [False, False, False]
Line 7
ROT_THREE
rotates the top three elements in the stack.
# TOS , TOS1
# B A DUP.B
stack = [False, False, False]
Line 8
The next instruction is COMPARE_OP(==)
, which compares if TOS == TOS1
,
pops TOS
and TOS1
before pushing the result of TOS == TOS1
.
# TOS , TOS1
# A==B, DUP.B
stack = [True, False]
Line 11
JUMP_IF_FALSE_OR_POP
jumps if TOS == False
, else it pops the stack and moves
on
# TOS
# DUP.B
stack = [False]
Huh, won't you look at that? The result of False == False
was thrown away
completely, leaving only the duplicate of B in the stack. This tells me that my
initial theory was completely wrong, python is not chaining these operations
together at all!
Lines 14-17
Now, the next set of instructions (14-17) builds the list, by pushing False
on
the stack and using BUILD_LIST(size)
to create the list.
# TOS , TOS1
# C B
stack = [List, False]
Line 20
COMPARE_OP(in)
tests whether TOS1
is in list TOS
, which is True
.
# TOS
stack = [True]
The line after that is RETURN_VALUE
, which returns the value of TOS
, which
is what we get when we run func()
Analysis
From what we can see, python does not chain these operations together as expected. It actually doesn't chain the operations together at all.
Looking at what values are in the stack at each instruction, we can see that
instead of keeping the result of A == B
, it keeps what we named as B
in the stack and tested whether or not B
was in the list C
. On line 11,
JUMP_IF_FALSE_OR_POP
was used. If A == B
were to evaluate to False
, it would end up jumping to line 24
and return False
.
Conclusion
In short, what the bytecode tells us is that the statement we wrote as
A == B in [C]
is interpreted as A == B and B in C
.
This must mean that given a statement structure as A is B is C
, the statement
is expanded into A is B and B is C
.