5

I am a beginner and have a confusion when I am learning python. If I have the following python code:

import numpy as np X = np.array([1,0,0]) Y = X X[0] = 2 print Y 

Y will be shown to be array([2, 0, 0])

However, if I do the following:

import numpy as np X = np.array([1,0,0]) Y = X X = 2*X print Y 

Y is still array([1,0,0])

What is going on?

4
  • 1
    Here you modify the local variable X the change will not be reflected to Y since Y is not even aware X exists. Commented Feb 21, 2017 at 20:12
  • Thanks, sorry I was not clear in the post. I change the line X[0] = 2 to X = 2*X in the original code above. Commented Feb 21, 2017 at 20:15
  • 1
    Because assignment to a slice/indexing operation mutates the data structure, where as using the * operator generates a new data structure. Also, read and understand this: nedbatchelder.com/text/names.html Commented Feb 21, 2017 at 20:17
  • Thanks @juanpa.arrivillaga The document is very useful Commented Feb 21, 2017 at 20:38

4 Answers 4

8

think of it this way: the equals sign in python assigns references.

Y = X makes Y point to the same address X points to

X[0] = 2 makes x[0] point to 2

X = 2*X makes X point to a new thing, but Y is still pointing to the address of the original X, so Y is unchanged

this isn't exactly true, but its close enough to understand the principle

Sign up to request clarification or add additional context in comments.

1 Comment

Given the way np.ndarray objects work, "X[0] = 2 makes x[0] point to 2" is simply wrong, but won't downvote because of that last disclaimer.
5

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.

3 Comments

__setitem__ is missing some underscores
Also, __mul__ is a method just like __setitem__, so you are not really explaining why the multiplication does not propagate to Y.
No it calls __rmul__ :)
3

This is more about convention and names than reference and value.

When you assign:

Y = X 

Then the name Y refers to the object that the name X points to. In some way the pointer X and Y point to the same object:

X is Y # True 

The is checks if the names point to the same object!


Then it get's tricky: You do some operations on the arrays.

X[0] = 2 

That's called "item assignment" and calls

X.__setitem__(0, 2) 

What __setitem__ should do (convention) is to update some value in the container X. So X should still point to the same object afterwards.

However X * 2 is "multiplication" and the convention states that this should create a new object (again convention, you can change that behaviour by overwriting X.__mul__). So when you do

X = X * 2 

The name X now refers to the new object that X * 2 created:

X is Y # False 

Normally common libraries follow these conventions but it's important to highlight that you can completly change this!

5 Comments

I'd say it's more a question of implementation than convention. While convention may dictate one thing or the other, the implementation of numpy arrays is what defines the behavior of the operators in the end. In this case of course implementation follows convention.
Also, technically OP asked about 2 * X, so __rmul__, but again, for numpy it's commutative.
I modified my question title based on your suggestion. It's hard to even ask questions for beginner. Thanks!
@SunnyYang I actually think the original title was appropriate as well. It's just that Python follows the "consenting adults" guidelines. That makes it very hard to answer such questions generally, because as MadPhysicist pointed out that depends on the actual implementation, which in my opinion should follow the Python conventions. However in specific cases (and you used a numpy array as example) it's easy to answer because NumPy follows (most) python conventions.
@MadPhysicist You're right. But I think it's easier to explain the conventions than to discuss possible implementations. The conventions for these operations are pretty explicit in Pythons data model. However there are lots of implementations that can break these conventions (for example return self in __mul__). And yes, I cheated a bit with __mul__ but to explain how __mul__ and __rmul__ work with Python & NumPy would make an answer on itself, especially if subclasses and __array_priority__ is involved.
2

When you say X = np.array([1, 0, 0]), you create an object that has some methods and some internal buffers that contain the actual data and other information in it.

Doing Y = X sets Y to refer to the same actual object. This is called binding to a name in Python. You have bound the same object that was bound to X to the name Y as well.

Doing X[0] = 2 calls the object's __setitem__ method, which does some stuff to the underlying buffers. If modifies the object in place. Now when you print the values of either X or Y, the numbers that come out of that object's buffers are 2, 0, 0.

Doing X = 2 * X translates to X.__rmul__(2). This method does not modify X in place. It creates and returns a new array object, each of whose elements is twice the corresponding element of X. Then you bind the new object to the name X. However, the name Y is still bound to the original array because you have not done anything to change that. As an aside, X.__rmul__ is used because 2.__mul__(X) does not work. Numpy arrays naturally define multiplication to be commutative, so X.__mul__ and X.__rmul__ should to the same thing.

It is interesting to note that you can also do X *= 2, which will propagate the changes to Y. This is because the *= operator translates to the __imul__ method, which does modify the input in place.

1 Comment

Thanks for pointing out the X*=2. I have to keep it in mind or!

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.