2

Can anybody explain what's going on here? Why does this happen?

>>> b = "1984"
>>> a = b, c = "AB"
>>> print(a, b, c)
'AB', 'A', 'B'

This behavior really blows my mind. Found this here

Way Too Simple
  • 275
  • 1
  • 2
  • 10
  • 1
    I'm no expert in the internals of Python but it appears the second line is being parsed as `a = (b, c = "AB")` – Robin Zigmond Dec 10 '19 at 22:19
  • 1
    @RobinZigmond Actually it's more equivalent to `a = "AB"` and then `b, c = a`. – blhsing Dec 10 '19 at 22:24
  • `a` takes the value of the right-most item, and apparently it's ok to unpack `b, c = "AB"`, so this is a combination of both behaviors. – Guimoute Dec 10 '19 at 22:24
  • 2
    because `a = b = "value"` is useful, and `[a,b] = (1,2)` is useful so those both are supported. You are just doing both expecting it to have the same behaviour as using semicolons. `a = 1 ; b = 2` is the reasonable way to do multiple independent assignments on the same line. – Tadhg McDonald-Jensen Dec 10 '19 at 22:24

3 Answers3

3

Assignment is a statement; it's defined to assign the far right side to the various targets from left to right. The rather dry language grammar description is:

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.

So for example:

a = b = 1

assigns 1 to a, then assigns it again to b, roughly the same as if you did:

__specialtmp = 1
a = __specialtmp
b = __specialtmp

where __specialtmp is an unnamed temporary storage location (on CPython, it's just loaded on the top of the program stack, then duplicated into two references, then each reference is popped off for assignment).

This just adds iterable unpacking to the mix; expanding your code the same way, it would look like:

__specialtmp = "AB"
a = __specialtmp  # Assigns original value to a
b, c = __specialtmp  # Unpacks string as iterable of its characters, assigning "A" to b, and "B" to c

This won't always work mind you; if the thing being unpacked is an iterator, and you assign to the unpacked names first, the iterator will be exhausted and nothing useful will be available for the second assignment:

b, c = [*a] = iter("AB")

This unpacks "A" to b, and "B" to c, but when it gets to a, which in plain [*a] = iter("AB") would become ["A", "B"] (the star syntax capturing "remaining" values to a list), in this case, the iterator gets exhausted populating b and c and a gets nothing (the empty list, []).

Point is, while this trick works, I wouldn't recommend it in general. Initializing multiple names to the same immutable value is fine, but it's liable to bite you otherwise.

ShadowRanger
  • 143,180
  • 12
  • 188
  • 271
  • Is `__specialtmp` always the right-most value? – roganjosh Dec 10 '19 at 22:29
  • @roganjosh: It's the result of the expression following the right-most `=` delimiter (obviously if `==` or `:=` or string literals or whatever get involved, it's not the last literal `=` character). – ShadowRanger Dec 10 '19 at 22:34
  • The "obviously" part was what I was pondering :) `==` would evaluate to a bool, so I can understand that as "the value" I was referring to, but `:=` I'm less sure about how it behaves because you could have duplicate names in the chain of `x = y = x` (I don't have a good grip on the walrus)? – roganjosh Dec 10 '19 at 22:40
  • @AlexandrShurigin: Nope. The "expression list" is the part to the right of the right-most equals sign (the result of which is assigned across the target list). The other part is the "target list". – ShadowRanger Dec 10 '19 at 22:47
  • @roganjosh: The walrus is an expression. The expression is completely evaluated before assignment to the targets begins, so `a = b = (c := 1)` assigns to `c`, then to `a`, then to `b`. – ShadowRanger Dec 10 '19 at 22:49
  • Sure. I'm not gonna keep pestering you, and if I had the latest python myself handy, I'd test it, but is `a = b = (b := 1)` valid? I upvoted anyway because I think you gave a very clear explanation, I'm just trying to keep my eyes open for weirdness before I tuck this away in memory – roganjosh Dec 10 '19 at 22:55
  • @roganjosh: Yeah, it's valid. Unlike C/C++, where a lot of evaluation and assignment order is undefined or implementation defined, Python defines it *very* strictly. The "expression" being assigned *must* be evaluated first (`a : = 1`), then the result is assigned to the left-most `a`, then `b`. The targets themselves are evaluated immediately prior to assignment, so if you do `mydict[a] = (a := 1)` then the original value of `a` is irrelevant; `a` becomes `1`, and then you map `1` to `1` in `mydict`. – ShadowRanger Dec 10 '19 at 23:05
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/204010/discussion-between-shadowranger-and-roganjosh). – ShadowRanger Dec 10 '19 at 23:06
  • No need, but thank you; I also asked in chat and `a = b = (b := 1) + 1` was the example given back that showed your answer still holds and cleared my doubt :). The fact that the walrus operator and `b` share the same name doesn't change the order you've shown. – roganjosh Dec 10 '19 at 23:09
1

Such a cool question! Makes a lot of fun! :) Can be used at interviews :)

Ok, here we are

>>> b = "1984"
>>> a = b, c = "AB"
>>> print((a,b,c))
('AB', 'A', 'B')
>>> a = (b, c) = "AB"
>>> print((a,b,c))
('AB', 'A', 'B')
>>>

In python for multiple assignments, you can omit (...) and it looks like python parses this line similar to 2 lines

a = "AB"
b, c = "AB" # which is equal to (b, c) = "AB"

Some more examples

>>> a = b, c = "AB"
>>> print((a,b,c))
('AB', 'A', 'B')
>>> a = (b, c) = "AB"
>>> print((a,b,c))
('AB', 'A', 'B')
>>> a = "AB"
>>> b, c = "AB"
>>> print((a,b,c))
('AB', 'A', 'B')
>>>

It works using lists a well :)

>>> a = [b, c] = 'AB'
>>> print((a,b,c))
('AB', 'A', 'B')
>>>

Some more examples:

Alexandr Shurigin
  • 3,921
  • 1
  • 13
  • 25
  • 1
    There are no `tuple`s involved here, and it's somewhat misleading to reference them; unpacking uses commas, but it doesn't make `tuple`s. For that matter, if `tuple`s *were* involved (on the right-hand side of the right-most equals sign), the parentheses wouldn't be necessary (it's not parentheses that make a non-empty `tuple`, it's commas, with the parentheses only needed to resolve precedence/ambiguity issues). – ShadowRanger Dec 10 '19 at 22:28
  • Thanks, fixed :) – Alexandr Shurigin Dec 10 '19 at 22:37
  • It's a common mistake (even in tutorials), but it's still a mistake. No `tuple`s are involved, either in the implementation or in the logical behavior. `a, b = b, a` does involve a logical `tuple` (on the right hand side) which the implementation omits, but the `a, b` part is an unrelated part of the grammar (that just happens to be mostly symmetrical with how `tuple`s are used in the expression on the right hand side). – ShadowRanger Dec 10 '19 at 22:45
  • No actual `list`s involved in `a = [b, c] = 'AB'`. The grammar lets you surround a "`target_list`" (grammar term unrelated to the `list` datatype) with parentheses or brackets, as you like, but it's still not a `list` or a `tuple`. You can read [the grammar description](https://docs.python.org/3/reference/simple_stmts.html#assignment-statements), but be warned, it's not intended for normal humans, it's intended for parsers, so the wording is weirdly strict and non-intuitive in many ways. – ShadowRanger Dec 10 '19 at 22:51
1

Let's make this a little simpler. Let's look at the following case

>>> b = 1
>>> b, c = (0, 2)
>>> print(b, c)
0, 2

Is it surprising that b is 0 and not 1? It shouldn't be since we assigning b to 0 and c to 2 when calling b, c = (0, 2) thanks to tuple unpacking.

Now to address the other part of the gotcha, let's take this example

>>> b = 1
>>> a = b = 0
>>> print (b)
0

Is it again surprising that b is 0 and not 1? Again, it shouldn't be since when calling a = b = 0, we've assigned both a and b to 0 with multiple assignment.

So coming back to the gotcha, the a = b, c = "AB" is just a combination of these two behaviors. b, c = "AB" will unpack "A" to b and "B" to c, and we're also assigning "AB" to a. While it looks like we're assigning a = b, we're really just doing the following two lines

>>> b = "1984"
>>> b, c = "AB"
>>> a = "AB"

Hopefully this breaks down where the tuple unpacking is happening and where the assignment is happening, and that it's not as confusing of a gotcha as it might look.

Joe Habel
  • 206
  • 1
  • 6