The Python Doc for Comparisons says:
Comparisons can be chained arbitrarily, e.g.,
x < y <= z
is equivalent tox < y and y <= z
, except thaty
is evaluated only once (but in both casesz
is not evaluated at all whenx < y
is found to be false).
And these SO questions/answers shed some more light on such usage:
So something like (contrived example):
if 1 < input("Value:") < 10: print "Is greater than 1 and less than 10"
only asks for input once. This makes sense. And this:
if 1 < input("Val1:") < 10 < input("Val2:") < 20: print "woo!"
only asks for Val2
if Val1
is between 1 & 10 and only prints "woo!" if Val2
is also between 10 and 20 (proving they can be 'chained arbitrarily'). This also makes sense.
But I'm still curious how this is actually implemented/interpreted at the lexer/parser/compiler (or whatever) level.
Is the first example above basically implemented like this:
x = input("Value:")
1 < x and x < 10: print "Is between 1 and 10"
where x
really only exists (and is actually essentially unnamed) for those comparisons? Or does it somehow make the comparison operator return both the boolean result and the evaluation of the right operand (to be used for further comparison) or something like that?
Extending analysis to the second example leads me to believe it's using something like an unnamed intermediate result (someone educate me if there's a term for that) as it doesn't evaluate all the operands before doing the comparison.
You can simply let Python tell you what bytecode is produced with the dis
module:
>>> import dis
>>> def f(): return 1 < input("Value:") < 10
...
>>> dis.dis(f)
1 0 LOAD_CONST 1 (1)
3 LOAD_GLOBAL 0 (input)
6 LOAD_CONST 2 ('Value:')
9 CALL_FUNCTION 1
12 DUP_TOP
13 ROT_THREE
14 COMPARE_OP 0 (<)
17 JUMP_IF_FALSE_OR_POP 27
20 LOAD_CONST 3 (10)
23 COMPARE_OP 0 (<)
26 RETURN_VALUE
>> 27 ROT_TWO
28 POP_TOP
29 RETURN_VALUE
Python uses a stack; the CALL_FUNCTION
bytecode uses items on the stack (the input
global and the 'Value:'
string) to call a function with one argument, to replace those two items on the stack with the result of the function call. Before the function call, the the constant 1
was loaded on the stack.
So by the time input
was called the stack looks like:
input_result
1
and DUP_TOP
duplicates the top value, before rotating the top three stack values to arrive at:
1
input_result
input_result
and a COMPARE_OP
to test the top two items with <
, replacing the top two items with the result.
If that result was False
the JUMP_IF_FALSE_OR_POP
bytecode jumps over to 27, which rotates the False
on top with the remaining input_result
to clear that out with a POP_TOP
, to then return the remaining False
top value as the result.
If the result True
however, that value is popped of the stack by the JUMP_IF_FALSE_OR_POP
bytecode and in it's place the 10
value is loaded on top and we get:
10
input_result
and another comparison is made and returned instead.
In summary, essentially Python then does this:
stack_1 = stack_2 = input('Value:')
if 1 < stack_1:
result = False
else:
result = stack_2 < 10
with the stack_*
values cleared again.
The stack, then, holds the unnamed intermediate result to compare