Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do chained comparisons in Python actually work?

The Python Doc for Comparisons says:

Comparisons can be chained arbitrarily, e.g., x < y <= z is equivalent to x < y and y <= z, except that y is evaluated only once (but in both cases z is not evaluated at all when x < y is found to be false).

And these SO questions/answers shed some more light on such usage:

  • Python comparison operators chaining/grouping left to right?
  • What does "evaluated only once" mean for chained comparisons in Python?, in particular the currently-accepted answer

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.

like image 836
johnny Avatar asked Feb 26 '15 23:02

johnny


People also ask

What is a chained comparison Python?

Chain of multiple comparisonsComparisons can be chained arbitrarily, e.g., x < y <= z is equivalent to x < y and y <= z , except that y is evaluated only once (but in both cases z is not evaluated at all when x < y is found to be false).

Does Python allow chained comparison?

Python supports chaining of comparison operators, which means if we wanted to find out if b lies between a and c we can do a < b < c , making code super-intuitive. Python evaluates such expressions like how we do in mathematics.

How do you use comparisons in Python?

Python comparison operators can be used to compare strings in Python. These operators are: equal to ( == ), not equal to ( != ), greater than ( > ), less than ( < ), less than or equal to ( <= ), and greater than or equal to ( >= ).

How does Python compare two operands?

These operators compare the values on either sides of them and decide the relation among them. They are also called Relational operators. If the values of two operands are equal, then the condition becomes true. (a == b) is not true.


1 Answers

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

like image 131
Martijn Pieters Avatar answered Oct 14 '22 02:10

Martijn Pieters