Difference between mutation, rebinding, copying value, and assignment operator [duplicate]
Asked Answered
G

5

11
#!/usr/bin/env python3.2

def f1(a, l=[]):
    l.append(a)
    return(l)

print(f1(1))
print(f1(1))
print(f1(1))

def f2(a, b=1):
    b = b + 1
    return(a+b)

print(f2(1))
print(f2(1))
print(f2(1))

In f1 the argument l has a default value assignment, and it is only evaluated once, so the three print output 1, 2, and 3. Why f2 doesn't do the similar?

Conclusion:

To make what I learned easier to navigate for future readers of this thread, I summarize as the following:

  • I found this nice tutorial on the topic.

  • I made some simple example programs to compare the difference between mutation, rebinding, copying value, and assignment operator.

Gripping answered 31/1, 2012 at 3:29 Comment(0)
D
6

This is covered in detail in a relatively popular SO question, but I'll try to explain the issue in your particular context.


When your declare your function, the default parameters get evaluated at that moment. It does not refresh every time you call the function.

The reason why your functions behave differently is because you are treating them differently. In f1 you are mutating the object, while in f2 you are creating a new integer object and assigning it into b. You are not modifying b here, you are reassigning it. It is a different object now. In f1, you keep the same object around.

Consider an alternative function:

def f3(a, l= []):
   l = l + [a]
   return l

This behaves like f2 and doesn't keep appending to the default list. This is because it is creating a new l without ever modifying the object in the default parameter.


Common style in python is to assign the default parameter of None, then assign a new list. This gets around this whole ambiguity.

def f1(a, l = None):
   if l is None:
       l = []

   l.append(a)

   return l
Dispense answered 31/1, 2012 at 3:55 Comment(3)
Re the "Common style in python is ..." bit: What if I do bomb = [1, 2, 3]; print(f1(4, bomb)); print(f1(5, bomb))?Undertake
@Undertake if that's a problem, then you shouldn't be using mutable data structures in the first place.Peele
My rule of thumb is "don't modify values passed in from outside (unless that's the pointof the function)". Adhering to that rule also makes the None default argument trick unnecessary. Any time you could accidentally modify a default means you could also accidentally modify an explicitly passed value, which usually isn't any better. Deliberately modifying a passed value is fine, of course.Undertake
K
5

Because in f2 the name b is rebound, whereas in f1 the object l is mutated.

Kitkitchen answered 31/1, 2012 at 3:31 Comment(2)
I think OP will be needing a better explanation.Hayward
You are right.. then I direct the OP to this related question -> #8998059Kitkitchen
U
3

This is a slightly tricky case. It makes sense when you have a good understanding of how Python treats names and objects. You should strive to develop this understanding as soon as possible if you're learning Python, because it is central to absolutely everything you do in Python.

Names in Python are things like a, f1, b. They exist only within certain scopes (i.e. you can't use b outside the function that uses it). At runtime a name refers to a value, but can at any time be rebound to a new value with assignment statements like:

a = 5
b = a
a = 7

Values are created at some point in your program, and can be referred to by names, but also by slots in lists or other data structures. In the above the name a is bound to the value 5, and later rebound to the value 7. This has no effect on the value 5, which is always the value 5 no matter how many names are currently bound to it.

The assignment to b on the other hand, makes binds the name b to the value referred to by a at that point in time. Rebinding the name a afterwards has no effect on the value 5, and so has no effect on the name b which is also bound to the value 5.

Assignment always works this way in Python. It never has any effect on values. (Except that some objects contain "names"; rebinding those names obviously effects the object containing the name, but it doesn't affect the values the name referred to before or after the change)

Whenever you see a name on the left side of an assignment statement, you're (re)binding the name. Whenever you see a name in any other context, you're retrieving the (current) value referred to by that name.


With that out of the way, we can see what's going on in your example.

When Python executes a function definition, it evaluates the expressions used for default arguments and remembers them somewhere sneaky off to the side. After this:

def f1(a, l=[]):
    l.append(a)
    return(l)

l is not anything, because l is only a name within the scope of the function f1, and we're not inside that function. However, the value [] is stored away somewhere.

When Python execution transfers into a call to f1, it binds all the argument names (a and l) to appropriate values - either the values passed in by the caller, or the default values created when the function was defined. So when Python beings executing the call f3(5), the name a will be bound to the value 5 and the name l will be bound to our default list.

When Python executes l.append(a), there's no assignment in sight, so we're referring to the current values of l and a. So if this is to have any effect on l at all, it can only do so by modifying the value that l refers to, and indeed it does. The append method of a list modifies the list by adding an item to the end. So after this our list value, which is still the same value stored to be the default argument of f1, has now had 5 (the current value of a) appended to it, and looks like [5].

Then we return l. But we've modified the default list, so it will affect any future calls. But also, we've returned the default list, so any other modifications to the value we returned will affect any future calls!

Now, consider f2:

def f2(a, b=1):
    b = b + 1
    return(a+b)

Here, as before, the value 1 is squirreled away somewhere to serve as the default value for b, and when we begin executing f2(5) call the name a will become bound to the argument 5, and the name b will become bound to the default value 1.

But then we execute the assignment statement. b appears on the left side of the assignment statement, so we're rebinding the name b. First Python works out b + 1, which is 6, then binds b to that value. Now b is bound to the value 6. But the default value for the function hasn't been affected: 1 is still 1!


Hopefully that's cleared things up. You really need to be able to think in terms of names which refer to values and can be rebound to point to different values, in order to understand Python.

It's probably also worth pointing out a tricky case. The rule I gave above (about assignment always binding names with no effect on the value, so if anything else affects a name it must do it by altering the value) are true of standard assignment, but not always of the "augmented" assignment operators like +=, -= and *=.

What these do unfortunately depends on what you use them on. In:

x += y

this normally behaves like:

x = x + y

i.e. it calculates a new value with and rebinds x to that value, with no effect on the old value. But if x is a list, then it actually modifies the value that x refers to! So be careful of that case.

Undertake answered 31/1, 2012 at 6:6 Comment(5)
Thank you very much for spending much ink on explaining the details. I appreciate it. Also, I found this nice tutorial on the topic. Also, I made some simple example programs to compare the difference between mutation, rebinding, assignment operator, and copying value.Gripping
Perhaps OT, but is't this faulty design in the language? Just by reading it, I can't infer that [] survives anywhere - I'd just assume it scopes out UNLESS "l" gets stored somwhere. (sort of l=new Array). The limbic existence of [] seems pathological to me.Scent
@AlienLifeForm: I don't think so. Either the value for the default or the code to produce it has to be stored somewhere off to the side. The default expression doesn't have to be a simple literal, it can be any Python expression with arbitrarily complex runtime and side-effects. It would be much less intuitive if it was stored off to the side as code that is silently executed every time the function is called. As it is, default values are basically data attached to the function object; in fact they're actually accessible as the func_defaults attribute of a function.Undertake
Great answer. Perhaps you could reference a quick example of when the assignment operator isn't used this way in C++, for example. (I mean, just state the example, don't waste your time explaining it.) I am trying to think of such a case, but I'm a beginner at this point.Unconventional
@EricAuld Hmm. It's been a while since I did much C or C++, but I think the main complication there is that memory locations are explicitly part of the language via pointers (and to an extent references). So to a certain extent you have to deal with values "really changing" when you assign even simple integers, because the value of a memory location really changes when you do that, and C/C++ can see and talk about the memory location. I'm not really sure how to make a good example that wouldn't just add too much noise to a Python discussion.Undertake
O
0

In f1 you are storing the value in an array or better yet in Python a list where as in f2 your operating on the values passed. Thats my interpretation on it. I may be wrong

Ous answered 31/1, 2012 at 3:54 Comment(0)
U
0

Other answers explain why this is happening, but I think there should be some discussion of what to do if you want to get new objects. Many classes have the method .copy() that allows you create copies. For instance, if we rewrite f1 as

def f1(a, l=[]):
    new_l = l.copy()
    new_l.append(a)
    return(new_l)

then it will continue to return [1]no matter how many times we call it. There is also the library https://docs.python.org/3/library/copy.html for managing copies.

Also, if you're looping through the elements of a container and mutating them one by one, using comprehensions not only is more Pythonic, but can avoid the issue of mutating the original object. For instance, suppose we have the following code:

data = [1,2,3]
scaled_data = data
for i, value in enumerate(scaled_data):
     scaled_data[i] = value/sum(data)

This will set scaled_data to [0.16666666666666666, 0.38709677419354843, 0.8441754916792739]; each time you set a value of scaled_data to the scaled version, you also change the value in data. If you instead have

data = [1,2,3]
scaled_data = [x/sum(data) for x in data]

this will set scaled_data to [0.16666666666666666, 0.3333333333333333, 0.5] because you're not mutating the original object but creating a new one.

Unlatch answered 3/2, 2022 at 16:50 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.