Take a look at the python byte code (outputs of dis) when we define the following three functions:
In [187]: def b():
a[pos1][pos2]=2
return a
In [188]: dis.dis(b)
2 0 LOAD_CONST 1 (2)
3 LOAD_GLOBAL 0 (a)
6 LOAD_GLOBAL 1 (pos1)
9 BINARY_SUBSCR
10 LOAD_GLOBAL 2 (pos2)
13 STORE_SUBSCR
3 14 LOAD_GLOBAL 0 (a)
17 RETURN_VALUE
In [189]: b()
Out[189]: array([ 1., 0., 1., 0., 1., 0., 1., 0., 1., 0.])
In [190]: def c():
e=a.copy()
e[pos1][pos2]=2
return e
In [191]: dis.dis(c)
2 0 LOAD_GLOBAL 0 (a)
3 LOAD_ATTR 1 (copy)
6 CALL_FUNCTION 0
9 STORE_FAST 0 (e)
3 12 LOAD_CONST 1 (2)
15 LOAD_FAST 0 (e)
18 LOAD_GLOBAL 2 (pos1)
21 BINARY_SUBSCR
22 LOAD_GLOBAL 3 (pos2)
25 STORE_SUBSCR
4 26 LOAD_FAST 0 (e)
29 RETURN_VALUE
In [191]: c()
Out[191]: array([ 1., 0., 1., 0., 1., 0., 1., 0., 1., 0.])
In [192]: def d():
f=a[pos1]
f[pos2]=2
return f
In [193]: dis.dis(d)
2 0 LOAD_GLOBAL 0 (a)
3 LOAD_GLOBAL 1 (pos1)
6 BINARY_SUBSCR
7 STORE_FAST 0 (f)
3 10 LOAD_CONST 1 (2)
13 LOAD_FAST 0 (f)
16 LOAD_GLOBAL 2 (pos2)
19 STORE_SUBSCR
4 20 LOAD_FAST 0 (f)
23 RETURN_VALUE
In [194]: d()
Out[194]: array([ 2., 1., 2., 1., 2.])
From the disassembled code, each time the a[pos1][pos2]=2 assignment is performed, it is indeed stored in the top of the stack but then, global (case 1) or the local (case 2) variables are returned instead.
When you split the operations (case 3), the interpreter seems to all at sudden remember that it had just stored the value on the stack and does not need to reload it.
a[pos1]. You would have to dig into the docs or maybe even the source to find the answer.