How Do Chained Assignments Work

How do chained assignments work?

They will not necessarily work the same if somefunction returns a mutable value. Consider:

>>> def somefunction():
... return []
...
>>> x = y = somefunction()
>>> x.append(4)
>>> x
[4]
>>> y
[4]
>>> x = somefunction(); y = somefunction()
>>> x.append(3)
>>> x
[3]
>>> y
[]

Why does chained assignment work this way?

Per the language docs on assignment:

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.

In this case, a = a[1:] = [2] has an expression list [2], and two "target lists", a and a[1:], where a is the left-most "target list".

You can see how this behaves by looking at the disassembly:

>>> import dis
>>> dis.dis('a = a[1:] = [2]')
1 0 LOAD_CONST 0 (2)
2 BUILD_LIST 1
4 DUP_TOP
6 STORE_NAME 0 (a)
8 LOAD_NAME 0 (a)
10 LOAD_CONST 1 (1)
12 LOAD_CONST 2 (None)
14 BUILD_SLICE 2
16 STORE_SUBSCR
18 LOAD_CONST 2 (None)
20 RETURN_VALUE

(The last two lines of the disassembly can be ignored, dis is making a function wrapper to disassemble the string)

The important part to note is that when you do x = y = some_val, some_val is loaded on the stack (in this case by the LOAD_CONST and BUILD_LIST), then the stack entry is duplicated and assigned, from left to right, to the targets given.

So when you do:

a = a[1:] = [2]

it makes two references to a brand new list containing 2, and the first action is a STORE one of these references to a. Next, it stores the second reference to a[1:], but since the slice assignment mutates a itself, it has to load a again, which gets the list just stored. Luckily, list is resilient against self-slice-assignment, or we'd have issues (it would be forever reading the value it just added to add to the end until we ran out of memory and crashed); as is, it behaves as a copy of [2] was assigned to replace any and all elements from index one onwards.

The end result is equivalent to if you'd done:

_ = [2]
a = _
a[1:] = _

but it avoids the use of the _ name.

To be clear, the disassembly annotated:

Make list [2]:

  1           0 LOAD_CONST               0 (2)
2 BUILD_LIST 1

Make a copy of the reference to [2]:

              4 DUP_TOP

Perform store to a:

              6 STORE_NAME               0 (a)

Perform store to a[1:]:

              8 LOAD_NAME                0 (a)
10 LOAD_CONST 1 (1)
12 LOAD_CONST 2 (None)
14 BUILD_SLICE 2
16 STORE_SUBSCR

How does this chain assignment work?

On line 3 you have a chained assignment

t = A[t] = A.count(3)

t = A.count(3) is evaluated first – t set to the return value of A.count(3) which in this case is 1.
Then the member of A at index t(=1) is set to the return value of A.count(3), which is still 1.

Read more about chained assignments in Python here

Chained assignment for mutable types

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.

What is this kind of assignment in Python called? a = b = True

It's a chain of assignments and the term used to describe it is...

- Could I get a drumroll please?

Chained Assignment.


I just gave it a quite google run and found that there isn't that much to read on the topic, probably since most people find it very straight-forward to use (and only the true geeks would like to know more about the topic).


In the previous expression the order of evaluation can be viewed as starting at the right-most = and then working towards the left, which would be equivalent of writing:

b = True
a = b

The above order is what most language describe an assignment-chain, but python does it differently. In python the expression is evaluated as this below equivalent, though it won't result in any other result than what is previously described.

temporary_expr_result = True

a = temporary_expr_result
b = temporary_expr_result

Further reading available here on stackoverflow:

  • How do chained assignments work? python

Why does the order matter in this chained assignment?

You basically answered your question at the end.

node = tmp
node[ch] = tmp

and

node[ch] = tmp
node = tmp

are not equivalent. As soon as you do node = tmp, you lose the previous contents of node, and replace them with a new dict. That means that in the first case, you immediately lose the reference to trie inside the loop, and can no longer mutate it. In the second case, you alter the old results, then reassign node to the new dictionary.

Assignment operator chain understanding

a += a means a = a + a.

likewise, a -= a means a = a - a.

I am not sure which way is proper to start, but if I convert the given code from the right using above,

   a += a                     >     a = a + a;
a -= a += a > a = a - (a + a);
a+= a -= a += a > a = a + (a - (a + a ));
a -= a+= a -= a += a > a = a - (a + (a - (a + a)));
a = a -= a+= a -= a += a > a = a - a - a + a + a;

where -a -a + a + a cancels each other, resulting a = a, which is 10.

Pandas: Chained assignments

The point of the SettingWithCopy is to warn the user that you may be doing something that will not update the original data frame as one might expect.

Here, data is a dataframe, possibly of a single dtype (or not). You are then taking a reference to this data['amount'] which is a Series, and updating it. This probably works in your case because you are returning the same dtype of data as existed.

However it could create a copy which updates a copy of data['amount'] which you would not see; Then you would be wondering why it is not updating.

Pandas returns a copy of an object in almost all method calls. The inplace operations are a convience operation which work, but in general are not clear that data is being modified and could potentially work on copies.

Much more clear to do this:

data['amount'] = data["amount"].fillna(data.groupby("num")["amount"].transform("mean"))

data["amount"] = data['amount'].fillna(mean_avg)

One further plus to working on copies. You can chain operations, this is not possible with inplace ones.

e.g.

data['amount'] = data['amount'].fillna(mean_avg)*2

And just an FYI. inplace operations are neither faster nor more memory efficient. my2c they should be banned. But too late on that API.

You can of course turn this off:

pd.set_option('chained_assignment',None)

Pandas runs with the entire test suite with this set to raise (so we know if chaining is happening) on, FYI.



Related Topics



Leave a reply



Submit