Aseem Bansal
Aseem Bansal

Reputation: 6962

Confusion regarding mutable and immutable data types in Python 2.7

I have read that while writing functions it is good practice to copy the arguments into other variables because it is not always clear whether the variable is immutable or not. [I don't remember where so don't ask]. I have been writing functions according to this.

As I understand creating a new variable takes some overhead. It may be small but it is there. So what should be done? Should I be creating new variables or not to hold the arguments?

I have read this and this. I have confusion regarding as to why float's and int's are immutable if they can be changed this easily?

EDIT:

I am writing simple functions. I'll post example. I wrote the first one when after I read that in Python arguments should be copied and the second one after I realized by hit-and-trial that it wasn't needed.

#When I copied arguments into another variable
def zeros_in_fact(num):
    '''Returns the number of zeros at the end of factorial of num'''
    temp = num
    if temp < 0:
        return 0
    fives = 0
    while temp:
        temp /=  5
        fives += temp
    return fives

#When I did not copy arguments into another variable
def zeros_in_fact(num):
    '''Returns the number of zeros at the end of factorial of num'''
    if num < 0:
        return 0
    fives = 0
    while num:
        num /=  5
        fives += num
    return fives

Upvotes: 0

Views: 728

Answers (3)

BrenBarn
BrenBarn

Reputation: 251368

What you are doing in your code examples involves no noticeable overhead, but it also doesn't accomplish anything because it won't protect you from mutable/immutable problems.

The way to think about this is that there are two kinds of things in Python: names and objects. When you do x = y you are operating on a name, attaching that name to the object y. When you do x += y or other augmented assignment operators, you also are binding a name (in addition to doing the operation you use, + in this case). Anything else that you do is operating on objects. If the objects are mutable, that may involve changing their state.

Ints and floats cannot be changed. What you can do is change what int or float a name refers to. If you do

x = 3
x = x + 4

You are not changing the int. You are changing the name x so that it now is attached to the number 7 instead of the number 3. On the other hand when you do this:

x = []
x.append(2)

You are changing the list, not just pointing the name at a new object.

The difference can be seen when you have multiple names for the same object.

>>> x = 2
>>> y = x
>>> x = x + 3 # changing the name
>>> print x
5
>>> print y # y is not affected
2
>>> x = []
>>> y = x
>>> x.append(2) # changing the object
>>> print x
[2]
>>> print y # y is affected
[2]

Mutating an object means that you alter the object itself, so that all names that point to it see the changes. If you just change a name, other names are not affected.

The second question you linked to provides more information about how this works in the context of function arguments. The augmented assignment operators (+=, *=, etc.) are a bit trickier since they operate on names but may also mutate objects at the same time. You can find other questions on StackOverflow about how this works.

Upvotes: 2

bbill
bbill

Reputation: 2304

I think it's best to keep it simple in questions like these.

The second link in your question is a really good explanation; in summary:

Methods take parameters which, as pointed out in that explanation, are passed "by value". The parameters in functions take the value of variables passed in.

For primitive types like strings, ints, and floats, the value of the variable is a pointer (the arrows in the following diagram) to a space in memory that represents the number or string.

code               | memory
                   |
an_int = 1         |    an_int ---->  1
                   |                  ^    
another_int = 1    |    another_int  /

When you reassign within the method, you change where the arrow points.

an_int = 2         |    an_int -------> 2
                   |    another_int --> 1

The numbers themselves don't change, and since those variables have scope only inside the functions, outside the function, the variables passed in remain the same as they were before: 1 and 1. But when you pass in a list or object, for example, you can change the values they point to outside of the function.

a_list = [1, 2, 3]    |              1   2   3
                      |   a_list ->| ^ | ^ | ^ |
                      |              0   2   3
a_list[0] = 0         |   a_list ->| ^ | ^ | ^ |

Now, you can change where the arrows in the list, or object, point to, but the list's pointer still points to the same list as before. (There should probably actually only be one 2 and 3 in the diagram above for both sets of arrows, but the arrows would have gotten difficult to draw.)

So what does the actual code look like?

a = 5
def not_change(a):
  a = 6
not_change(a)
print(a) # a is still 5 outside the function

b = [1, 2, 3]
def change(b):
  b[0] = 0
print(b) # b is now [0, 2, 3] outside the function

Whether you make a copy of the lists and objects you're given (ints and strings don't matter) and thus return new variables or change the ones passed in depends on what functionality you need to provide.

Upvotes: 3

Ignacio Vazquez-Abrams
Ignacio Vazquez-Abrams

Reputation: 798676

If you are rebinding the name then mutability of the object it contains is irrelevant. Only if you perform mutating operations must you create a copy. (And if you read between the lines, that indirectly says "don't mutate objects passed to you".)

Upvotes: 1

Related Questions