Python Multiple Assignment Statements In One Line
Asked Answered
K

5

50

(Don't worry, this isn't another question about unpacking tuples.)

In python, a statement like foo = bar = baz = 5 assigns the variables foo, bar, and baz to 5. It assigns these variables from left to right, as can be proved by nastier examples like

>>> foo[0] = foo = [0]
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'foo' is not defined
>>> foo = foo[0] = [0]
>>> foo
[[...]]
>>> foo[0]
[[...]]
>>> foo is foo[0]
True

But the python language reference states that assignment statements have the form

(target_list "=")+ (expression_list | yield_expression)

and on assignment the expression_list is evaluated first and then the assigning happens.

So how can the line foo = bar = 5 be valid, given that bar = 5 isn't an expression_list? How are these multiple assignments on one line getting parsed and evaluated? Am I reading the language reference wrong?

Kalfas answered 22/8, 2015 at 13:9 Comment(2)
Notice the + in (target_list "=")+, which means one or more copies. In foo = bar = 5, there are two (target_list "=") productions, and the expression_list part is just 5.Cephalization
Aha! That's what I was missing. If you make this an answer I can accept it. Thank you!Kalfas
K
28

All credit goes to @MarkDickinson, who answered this in a comment:

Notice the + in (target_list "=")+, which means one or more copies. In foo = bar = 5, there are two (target_list "=") productions, and the expression_list part is just 5

All target_list productions (i.e. things that look like foo =) in an assignment statement get assigned, from left to right, to the expression_list on the right end of the statement, after the expression_list gets evaluated.

And of course the usual 'tuple-unpacking' assignment syntax works within this syntax, letting you do things like

>>> foo, boo, moo = boo[0], moo[0], foo[0] = moo[0], foo[0], boo[0] = [0], [0], [0]
>>> foo
[[[[...]]]]
>>> foo[0] is boo
True
>>> foo[0][0] is moo
True
>>> foo[0][0][0] is foo
True
Kalfas answered 22/8, 2015 at 16:9 Comment(0)
F
24

Mark Dickinson explained the syntax of what is happening, but the weird examples involving foo show that the semantics can be counter-intuitive.

In C, = is a right-associative operator which returns as a value the RHS of the assignment so when you write x = y = 5, y=5 is first evaluated (assigning 5 to y in the process) and this value (5) is then assigned to x.

Before I read this question, I naively assumed that roughly the same thing happens in Python. But, in Python = isn't an expression (for example, 2 + (x = 5) is a syntax error). So Python must achieve multiple assignments in another way.

We can disassemble rather than guess:

>>> import dis
>>> dis.dis('x = y = 5')
  1           0 LOAD_CONST               0 (5)
              3 DUP_TOP
              4 STORE_NAME               0 (x)
              7 STORE_NAME               1 (y)
             10 LOAD_CONST               1 (None)
             13 RETURN_VALUE

See this for a description of the byte code instructions.

The first instruction pushes 5 onto the stack.

The second instruction duplicates it -- so now the top of the stack has two 5s

STORE_NAME(name) "Implements name = TOS" according to the byte code documentation

Thus STORE_Name(x) implements x = 5 (the 5 on top of the stack), popping that 5 off the stack as it goes, after which STORE_Name(y) implements y = 5 with the other 5 on the stack.

The rest of the bytecode isn't directly relevant here.

In the case of foo = foo[0] = [0] the byte-code is more complicated because of the lists but has a fundamentally similar structure. The key observation is that once the list [0] is created and placed on the stack then the instruction DUP_TOP doesn't place another copy of [0] on the stack, instead it places another reference to the list. In other words, at that stage the top two elements of the stack are aliases for the same list. This can be seen most clearly in the somewhat simpler case:

>>> x = y = [0]
>>> x[0] = 5
>>> y[0]
5

When foo = foo[0] = [0] is executed, the list [0] is first assigned to foo and then an alias of the same list is assigned to foo[0]. This is why it results in foo being a circular reference.

Fournier answered 22/8, 2015 at 14:25 Comment(4)
I thought the same thing about right-associativity at first, too, but I don't think that's what python's doing. If it was, foo[0] = foo = [0] would be a valid python statement, but it's not. Rather, foo = foo[0] = [0] is a valid statement - one that's equivalent to foo = [0]; foo[0] = foo. So to use your example, x = y = z = 5 is getting evaluated weird-left-associatively as x = 5; y = 5; z = 5. The plot thickens...Kalfas
Interesting ideas! @MarkDickinson pointed out my misreading of the language reference above, though. Now the whole thing makes sense, along with the fact that you can do things like foo, boo = foo[0], boo[0] = [0], [0]Kalfas
@cvitkovm I figured out what was happening with the foo cases. The circular reference is set up because of how assignments involving lists copy references rather than the lists themselves. Thank you for posing such an interesting question.Fournier
Excellent answer! I switched gears from JavaScript where like C, assignment is situation from right-to-left so good to know Python is the opposite and goes from left-to-right. Also, JS returns the value after each assignment, but Python it is a syntax error. Thank you for pointing these things out!Kendakendal
V
11

https://docs.python.org/3/reference/simple_stmts.html#grammar-token-assignment_stmt

An assignment statement evaluates the expression list (remember that this can be a single expression or a comma-separated list, the latter yielding a tuple) and assigns the single resulting object to each of the target lists, from left to right.

Velma answered 22/8, 2015 at 16:24 Comment(1)
Well, that is nicely and concisely explained in the documentation :DCortez
U
6

bar = 5 is not an expression. The multiple assignment is a separate statement from the assignment statement; the expression is everything to the right of the right-most =.

A good way to think about it is that the right-most = is the major separator; everything to the right of it happens from left to right, and everything to the left of it happens from left to right as well.

Unjust answered 22/8, 2015 at 15:18 Comment(4)
Well I definitely agree that's how the code is being evaluated, but I'm already okay on what the code does. What I'm trying to figure out is how python is parsing statements like foo = bar = 5 when it seems like that syntax conflicts with what's specified in the language reference. Could you give a reference for where you found that "The multiple assignment is a separate statement from the assignment statement..."? I could only find 'Assignment Statements' in the language reference.Kalfas
It might work to check what exactly the cpython implementation does.Daughterinlaw
@cvitkovm I know, because I've contributed to the astor project that an assignment is stored as a single AST node, but possibly with multiple targets on the left. If you look at that link, you will see that reconstituting the source requires printing of an equals sign for each target.Unjust
Thanks @PatrickMaupin. Mark Dickinson pointed out above where I misread the language reference, but it's nice to know how it plays out in the AST.Kalfas
T
2

The assignment order is that right most value is assigned to the first variable from left to right. Please note below:

>>> foo[0] = foo = [1,2,3] # line 1
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  NameError: name 'foo' is not defined
>>> foo = foo[0] = [1,2,3] # line 2
>>> foo
[[...], 2, 3]

The assignment at line 1 fails because it is trying assign a value to foo[0] but foo is never initialized or defined so it fails. The assignment at line 2 works because foo is first initialized to be [1,2,3] and then foo[0] is assigned [1,2,3]

Tapioca answered 5/1, 2022 at 4:51 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.