1

Ran into this issue when debugging a piece of code. If was not aware of this behaviour previously.

foo = bar = [1, 2, 3]

hex(id(foo))
Out[121]: '0x1f315dafe48'
hex(id(bar))
Out[122]: '0x1f315dafe48'

Both "variables" are pointing to the same memory location. But now if one is changed, the other changes as well:

foo.append(4)

bar
Out[126]: [1, 2, 3, 4]

So essentially here we have two names assigned to the same variable/memory address. This is different from:

foo = [1, 2, 3]
bar = [1, 2 ,3]
hex(id(foo))
Out[129]: '0x1f315198448'
hex(id(bar))
Out[130]: '0x1f319567dc8'

Here a change to either foo or bar won't have any effect on the other one.

So my question is: why does this feature (chained assignment for mutable types) even exist in Python? Does it serve any purpose apart from giving you tools to shoot yourself in the foot?

NotAName
  • 3,821
  • 2
  • 29
  • 44
  • The behavior of `foo = bar = [1, 2, 3]` is consistent with the behavior or `bar = [1, 2, 3]` followed by `foo = bar`. – Mark Oct 29 '20 at 01:18
  • @MarkMeyer, agree. But why would you ever do this knowing that any change to foo will change bar as well? What is the point of having two names assigned to the same variable? – NotAName Oct 29 '20 at 01:24
  • 1
    I don't know. I'm pretty sure I've never used that assignment pattern in real life. On the other hand, I *really* don't want my code allocating (possibly) large collections without me explicitly making a copy. – Mark Oct 29 '20 at 01:26
  • 1
    If you call `list.sort(foo)`, you would rather hope that the `list.sort` function holds a reference to the same list as `foo`, not a copy of it; otherwise it would sort its own copy and `foo` remains unchanged. But of course there must be a local variable in `list.sort` and it is a different variable than `foo` (it's probably called `self`). So there are lots of reasons you would want two different variables to reference the same mutable object. – kaya3 Oct 29 '20 at 01:42

1 Answers1

2

It's useful for simple, common initializations like

foo = bar = baz = 0

so you don't have to write

foo = 0
bar = 0
baz = 0

Since it's a syntax feature, it's not really feasible to make it only work for immutable types. The parser can't tell whether the expression at the end will be a mutable or immutable type. You can have

def initial_value():
    if random.choice([True, False]):
        return []
    else:
        return 0

foo = bar = baz = initial_value()

initial_value() can return a mutable or immutable value. The parser for the assignment can't know what it will be.

There are lots of ways to shoot yourself in the foot with multiple references to mutable values, Python doesn't go out of its way to stop you. For some of the more common examples, see "Least Astonishment" and the Mutable Default Argument and List of lists changes reflected across sublists unexpectedly

You just have to remember that in a chained assignment, the value expression is only evaluated once. So your assignment is equivalent to

temp = [1, 2, 3]
foo = temp
bar = temp

rather than

foo = [1, 2, 3]
bar = [1, 2, 3]

See How do chained assignments work?

A more general rule to remember is that Python never makes copies of objects spontaneously, you always have to tell it to do so.

Barmar
  • 741,623
  • 53
  • 500
  • 612
  • This works for immutables since they are effectively pointers. If you do `foo += 1`, then `foo` will now point to a different memory address, while both `bar` and `baz` will still point to the original address where value 0 is stored. It should never cause issues with immutables, but mutable types are a different story. – NotAName Oct 29 '20 at 01:11
  • I'm not sure I agree with "parser can't tell whether the expression at the end will be mutable or immutable type". If the parser sees `[ ]` or `{ }` on the other side of assignment it should be instantly aware that it's dealing with a mutable. – NotAName Oct 29 '20 at 01:14
  • `foo = bar = baz = somefunc()` it depends on whether `somefunc()` returns a mutable or immutable object. – Barmar Oct 29 '20 at 01:14
  • `foo = bar = baz = [] if f() else 0` – Barmar Oct 29 '20 at 01:15
  • If `f()` is defined and can return a value parser should be aware of the returned value's type, shouldn't it? – NotAName Oct 29 '20 at 01:21
  • Functions don't have declared types. Also, the function needn't be defined at the time it's parsing the line that calls it. – Barmar Oct 29 '20 at 01:21
  • `def somefunc(): if random.choice([True, False]): return [] else: return 1` – Barmar Oct 29 '20 at 01:22