That's because X and Y are references to the same object np.array([1,0,0]) this means that regardless whether a call is done through X or Y, the result will be the same, but changing the reference of one, has no effect.
If you write:
X = np.array([1,0,0]) Y = X
basically what happens is that there are two local variables X and Y that refer to the same object. So the memory looks like:
+--------+ Y -> |np.array| <- X +--------+ |[1,0,0] | +--------+
Now if you do X[0] = 2 that is basically short for:
X.__setitem__(0,2)
so you call a method on the object. So now the memory looks like:
+--------+ Y -> |np.array| <- X +--------+ |[2,0,0] | +--------+
If you however write:
X = 2*X
first 2*X is evaluated. Now 2*X is short for:
X.__rmul__(2)
(Python first looks if 2 supports __mul__ for X, but since 2 will raise a NotImplementedException), Python will fallback to X.__rmul__). Now X.__rmul__ does not change X: it leaves X intact, but constructs a new array and returns that. X catches by that new array that now references to that array).
which creates an new array object: array([4, 0, 0]) and then X references to that new object. So now the memory looks like:
+--------+ +--------+ Y -> |np.array| X ->|np.array| +--------+ +--------+ |[2,0,0] | |[4,0,0] | +--------+ +--------+
But as you can see, Y still references to the old object.
Xthe change will not be reflected toYsinceYis not even awareXexists.*operator generates a new data structure. Also, read and understand this: nedbatchelder.com/text/names.html