[everything is this response is from 3.12.2]
AFAICT flat x or y or z
is compiled in 3.12 same x or (y or z)
.
Python 3.12.2 (main, Mar 12 2024, 11:02:14) [GCC 12.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import dis
>>> dis.dis('x or (y or z)')
0 0 RESUME 0
1 2 LOAD_NAME 0 (x)
4 COPY 1
6 POP_JUMP_IF_TRUE 8 (to 24)
8 POP_TOP
10 LOAD_NAME 1 (y)
12 COPY 1
14 POP_JUMP_IF_TRUE 3 (to 22)
16 POP_TOP
18 LOAD_NAME 2 (z)
20 RETURN_VALUE
>> 22 RETURN_VALUE
>> 24 RETURN_VALUE
That looks like a sensible optimization to me — ignoring “broken” objects, or
is associative:
>>> for x,y,z in itertools.product([0,1], repeat=3):
... print(x,y,z, '(x or y) or z =', (x or y) or z, 'x or (y or z) =', x or (y or z))
...
0 0 0 (x or y) or z = 0 x or (y or z) = 0
0 0 1 (x or y) or z = 1 x or (y or z) = 1
0 1 0 (x or y) or z = 1 x or (y or z) = 1
0 1 1 (x or y) or z = 1 x or (y or z) = 1
1 0 0 (x or y) or z = 1 x or (y or z) = 1
1 0 1 (x or y) or z = 1 x or (y or z) = 1
1 1 0 (x or y) or z = 1 x or (y or z) = 1
1 1 1 (x or y) or z = 1 x or (y or z) = 1
- Same for
(x and y) and z
vs. x and (y and z)
.
- However mixing is NOT associative e.g.
(x or y) and z
!= x or (y and z)
.
But it’s not enough to be “sensible optimization”; Python strives for predictable behavior, so does this match parser, and does this match official grammar?
Well, AST surprised me
it’s neither this associativity nor that, but a single flat “BoolOp with 3 children” :
>>> print(ast.dump(ast.parse('(x or y) or z'), indent=2))
Module(
body=[
Expr(
value=BoolOp(
op=Or(),
values=[
BoolOp(
op=Or(),
values=[
Name(id='x', ctx=Load()),
Name(id='y', ctx=Load())]),
Name(id='z', ctx=Load())]))],
type_ignores=[])
>>> print(ast.dump(ast.parse('x or (y or z)'), indent=2))
Module(
body=[
Expr(
value=BoolOp(
op=Or(),
values=[
Name(id='x', ctx=Load()),
BoolOp(
op=Or(),
values=[
Name(id='y', ctx=Load()),
Name(id='z', ctx=Load())])]))],
type_ignores=[])
>>> print(ast.dump(ast.parse('x or y or z'), indent=2))
Module(
body=[
Expr(
value=BoolOp(
op=Or(),
values=[
Name(id='x', ctx=Load()),
Name(id='y', ctx=Load()),
Name(id='z', ctx=Load())]))],
type_ignores=[])
- Similarly
x and y and z
is a flat “And with 3 children”.
- (This is not new — python 3.11.8 also parses flat 3-children.)
- Mixes can not be flattened.
x and y or z
is parsed as '(x and y) or z'
BUT x or y and z
is parsed as x or (y and z)
— that is, and
has higher precendence.
What about grammar?
- 6.17. Operator precedence confirms
and
has higher precedence.
- 6.11. Boolean operations explains that
x or y
, x and y
do short-circuiting, but not how exactly it behaves for longer chains.
It gives grammar:or_test ::= and_test | or_test "or" and_test
and_test ::= not_test | and_test "and" not_test
not_test ::= comparison | "not" not_test
- Grammar agrees “and” has higher precendence.
- Grammar gives no hint that “BoolOp with 3 children” is a possibility, but I’m not sure if precise AST structure must match grammar, as long as it’s equivalent?
- Anyway, that grammar is left-recursive. IIUC it implies this parsing:
x "or" y "or" z
and_test(x) "or" and_test(y) "or" and_test(z)
or_test(and_test(x) "or" and_test(y)) "or" and_test(z)
or_test(or_test(and_test(x) "or" and_test(y)) "or" and_test(z))
So if I read that right, documented grammar requires (x or y) or z
associativity which contradicts x or (y or z)
execution.
If optimization is desired, it should be at least documented.