Python Comparison Operators Chaining/Grouping Left to Right

What does it mean that Python comparison operators chain/group left to right?

Grouping (this is what non-comparison operators do):

a + b + c   means   (a + b) + c

Chaining (this is what comparison operators do):

a < b < c   means   (a < b) and (b < c)

Grouping left to right (this is the way things are grouped):

5 - 2 - 1   means   (5 - 2) - 1 == 2

as opposed to grouping right to left (this would produce a different result):

5 - (2 - 1) == 4

Chaining left to right

Chaining is left to right, so in a < b < c, the expression a < b is evaluated before b < c, and if a < b is falsey, b < c is not evaluated.

(2 < 1 < f()) gives the value False without calling the function f, because 2 < 1 evaluates to false, so the second comparison does not need to be performed.

f() > 1 > g() calls f() in order to evaluate the first comparison, and depending on the result, it might or might not need to evaluate the second condition, which requires calling g().

NB. Each operand is evaluated at most once. So in the expression 1 < f() < 2, the function f() is only called once, and the value it gives is used in both comparisons (if necessary).

https://en.wikipedia.org/wiki/Short-circuit_evaluation

Precedence of chained comparisons?

Python has an Abstract Syntax Tree module to show you what's happening:

import ast
t = ast.parse('not a < b < c')
print(ast.dump(t))

It gives (cleaned up a bit):

[Expr(value=UnaryOp(
op=Not(),
operand=Compare(
left=Name(id='a'),
ops=[Lt(), Lt()],
comparators=[Name(id='b'), Name(id='c')]
)
))]

And indeed, the documentation says that not has lower precedence than <.

How do chained comparisons in Python actually work?

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

Why 5 = -15 == 5 = 1 != 20 is False?

Python's operator precedence is documented here. Contrary to what you claim, all of those comparison operators have the same precedence.

Normally, one looks to associativity to handle ties in precedence. But the comparison operators are special. Python supports chaining. This means that

x relop1 y relop2 z

is short for

x relop1 y and y relop2 z

(Except y is only evaluated once.)

So,

    5 <= -15 == 5 >= 1 != 20
= ( 5 <= -15 ) and ( -15 == 5 ) and ( 5 >= 1 ) and ( 1 != 20 )
= False and False and True and True
= False

How a single parenthesis could change the output?

Because of Python's comparison chaining feature.

3 in [1, 2, 3] == [1, 2, 3]

is treated as

(3 in [1, 2, 3]) and ([1, 2, 3] == [1, 2, 3])

Comparison operators and 'is' - operator precedence in python?

You're seeing python's operator chaining working

5 > 2 is True

Is equivalent to

5>2 and 2 is True

You can see this in that

>>> 5>2 is 2

Returns True.

In which order are two or more consecutive membership operators treated in Python?

This is operator chaining and will expand to 1 in g and g in g. So only after you append g to itself this becomes true.

You can use parentheses to get the behavior you want: (1 in g) in g. This forces the 1 in g part to be evaluated first (however in compares for equality and True == 1 so True doesn't need to be part of the list actually).



Related Topics



Leave a reply



Submit