Search code examples
pythoncomparison-operatorspython-internals

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:

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.


Solution

  • 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