Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Python operator precedence with augmented assignment

It seems this question only answered for Java but I would like to know how it works in Python. So are these the same?

a += b / 2

and

a += (b / 2)
like image 427
Manngo Avatar asked Sep 25 '18 18:09

Manngo


2 Answers

Yes, those are the same. Python's augmented assignment is not an expression, it is a statement, and doesn't play in expression precedence rules. += is not an operator, and instead it's part of the augmented assignment statement syntax.

So everything to the right of the += is an expression, but += itself is not, so the assignment will always be handled last.

And because (augmented) assignment is not an expression, it can't produce a value to use in a surrounding expression either. There is no (a += b) / 2, that'd be a syntax error, and certainly no if (a += b / 2): or other such shenanigans.

See the reference documentation on Augmented assignment statements, which states the grammar is:

augmented_assignment_stmt ::=  augtarget augop (expression_list | yield_expression)
augtarget                 ::=  identifier | attributeref | subscription | slicing
augop                     ::=  "+=" | "-=" | "*=" | "@=" | "/=" | "//=" | "%=" | "**="
                           | ">>=" | "<<=" | "&=" | "^=" | "|="

So the augop is part of the statement syntax, and only the part following is an expression (specifically, either a expression_list or yield_expression grammar rule).

Furthermore, the explanation shows:

An augmented assignment evaluates the target (which, unlike normal assignment statements, cannot be an unpacking) and the expression list, performs the binary operation specific to the type of assignment on the two operands, and assigns the result to the original target. The target is only evaluated once.

So the augtarget part is handled first, the expression list (or yield expression) is handled second, and then the augmented assignment applies the operator and assigns back the result.

Furthermore, the expressions reference documentation does include a precedence table, but that table doesn't include assignments (augmented or otherwise), simply because assignments are not expressions but statements.

like image 167
Martijn Pieters Avatar answered Nov 14 '22 16:11

Martijn Pieters


Short answer: += is an augmented assignment, and if we take the grammar into account, this is parsed higher in the syntax tree, than the operators in general (and hence the / operator in particular).

Python sees the += as an "augmented assignment". If we inspect the Python grammar we see:

augassign: ('+=' | '-=' | '*=' | '@=' | '/=' | '%=' | '&=' | '|=' | '^=' |
            '<<=' | '>>=' | '**=' | '//=')

Now the grammar also enforces the priority rules when parsing. If we look at the grammar that is related to stmt ("statement"), we see:

stmt: simple_stmt | compound_stmt
simple_stmt: small_stmt (';' small_stmt)* [';'] NEWLINE
small_stmt: (expr_stmt | del_stmt | pass_stmt | flow_stmt |
             import_stmt | global_stmt | nonlocal_stmt | assert_stmt)
expr_stmt: testlist_star_expr (annassign | augassign (yield_expr|testlist) |
                     ('=' (yield_expr|testlist_star_expr))*)

Exhaustively explaining all the other statements (like the del_statement) would take too long, but the expr_stmt is the only one that leads to an augassign (and augassign is the only variable that results in a += token). So we can ignore the other expressions.

Now if we "specialize" the expression of expr_stmt such that it has an augassign in it, we retrieve the production rule:

expr_stmt: testlist_star_expr augassign (yield_expr|testlist)

The testlist_star_expr is a variable that results in an identifier (or multiple identifiers in case of sequence unpacking), etc.

On the right we see a yield_expr, or a test_list. A test_list can result in comma separated expressions, with:

testlist: test (',' test)* [',']

This test allows to write ternary operators, but that is not mandatory:

test: or_test ['if' or_test 'else' test] | lambdef

We can take the or_test variable, which is used to group expressions with an or separator (again optional), since the or has the highest precedence.

or_test: and_test ('or' and_test)*

Then follows the and_test which, as the name probably suggests, allows us to write and operators:

and_test: not_test ('and' not_test)*

then follows the not operator (with the not_test):

not_test: 'not' not_test | comparison

We can have an arbitrary number of nots in front, but eventually we will pick the comparison.

If we look at the production roule for the comparison, we see:

comparison: expr (comp_op expr)*

This thus allows comparator chaining, like x <= y < z, next we look at the expr:

expr: xor_expr ('|' xor_expr)*
xor_expr: and_expr ('^' and_expr)*
and_expr: shift_expr ('&' shift_expr)*
shift_expr: arith_expr (('<<'|'>>') arith_expr)*
arith_expr: term (('+'|'-') term)*
term: factor (('*'|'@'|'/'|'%'|'//') factor)*

So this defines precedence rules, and we see that | takes precedence over ^, that takes precedence over &, and so on until we see a term is a sequence of factors with operators of '*', '@', '/', '%', and //, so here we finally "consume" our *. This thus means that the / is lower in the syntax tree than the += node.

Hence the way Python parses this expression is:

a += (b / 2)
like image 27
Willem Van Onsem Avatar answered Nov 14 '22 15:11

Willem Van Onsem