Why can a function modify some arguments as perceived by the caller, but not others?
Asked Answered
B

13

262

I'm trying to understand Python's approach to variable scope. In this example, why is f() able to alter the value of x, as perceived within main(), but not the value of n?

def f(n, x):
    n = 2
    x.append(4)
    print('In f():', n, x)

def main():
    n = 1
    x = [0,1,2,3]
    print('Before:', n, x)
    f(n, x)
    print('After: ', n, x)

main()

Output:

Before: 1 [0, 1, 2, 3]
In f(): 2 [0, 1, 2, 3, 4]
After:  1 [0, 1, 2, 3, 4]

See also:

Brant answered 22/2, 2009 at 16:42 Comment(3)
well explained here nedbatchelder.com/text/names.htmlMarriage
@Marriage that material has been updated: nedbatchelder.com/text/names1.htmlSenecal
Because you're rebinding n to a new name with n = 2, but you aren't doing that with x.Uralite
B
303

Some answers contain the word "copy" in the context of a function call. I find it confusing.

Python doesn't copy objects you pass during a function call ever.

Function parameters are names. When you call a function, Python binds these parameters to whatever objects you pass (via names in a caller scope).

Objects can be mutable (like lists) or immutable (like integers and strings in Python). A mutable object you can change. You can't change a name, you just can bind it to another object.

Your example is not about scopes or namespaces, it is about naming and binding and mutability of an object in Python.

def f(n, x): # these `n`, `x` have nothing to do with `n` and `x` from main()
    n = 2    # put `n` label on `2` balloon
    x.append(4) # call `append` method of whatever object `x` is referring to.
    print('In f():', n, x)
    x = []   # put `x` label on `[]` ballon
    # x = [] has no effect on the original list that is passed into the function

Here are nice pictures on the difference between variables in other languages and names in Python.

Blabbermouth answered 22/2, 2009 at 18:6 Comment(19)
This article helped me to understand the problem better and it suggests a workaround and some advanced uses: Default Parameter Values in PythonPolyvinyl
@Gfy, I've seen similar examples before but to me it doesn't describe a real-world situation. If you're modifying something that's passed in it doesn't make sense to give it a default.Forejudge
@MarkRansom, I think it does make sense if you want to provide optional output destination as in: def foo(x, l=None): l=l or []; l.append(x**2); return l[-1].Jamajamaal
For the last line of Sebastian's code, it said "# the above has no effect on the original list ". But in my opinion, it only has no effect on "n", but changed the "x" in the main() function. Am I correct?Norward
@user17670: x = [] in f() has no effect on the list x in the main function. I've updated the comment, to make it more specific.Blabbermouth
link http://python.net/~goodger/projects/pycon/2007/idiomatic/handout.html#other-languages-have-variables is broken, any chance someone has an updated one, or put some of those pictures in the post?Menken
I realize I'm a little late to the game, but I want to point out an important point that your answer doesn't mention. In fact, it seems to contradict it. The OP's question, and the example code given, are about scopes/namespaces in python. You can call member functions of objects passed as parameters that modify values (such as append() for lists), but you cannot actually change the value of the variable - it will only be changed for the scope of the function, and not for the global/main function scope.Demarche
@ThaddaeusMarkle “change the value of the variable” is imprecise/ambiguous terminology that should be avoided when discussing difference between “naming and binding” and “mutability of an object” in Python (it is ok to use it in general when it doesn’t lead to confusion)Blabbermouth
The "other languages have variables, Python has names" thing is really a misconception about variables and references. Python has variables, and they behave exactly the same as variables in almost every other language, it's just that all values in Python are references to objects. Every other language that has objects and references allows you to have a variable which holds a reference to an object, and Python's variables behave the same as those; for example, Java variables of type Object likewise can only hold a reference to an object.Conclusion
@Conclusion it is a mistake to treat it as misconception. Don't confuse a concept and its possible implementation. The model of labels (names) put on balloons (objects) is almost trivial (radically simple) and at the same time complete (!). Naturally, you can use concepts from other languages (e.g., it is possible to implement Python in Java). Though the distinction doesn't matter much unless we are troubleshooting naming issues specifically.Blabbermouth
But variables are not a "concept from other languages", the word "variable" is used repeatedly throughout the official Python documentation (e.g. here). It is not a matter of implementation; the semantics of Python's variables are the same as the semantics of variables in other languages. The misconception is the belief that they are not. The mental model of attaching names to balloons may be viable (as an educator myself I have opinions about this which I won't get into), but it is not specific to Python; that's the point.Conclusion
@Conclusion yes, most of the time, it is ok to use the term variable unless we are discussing naming and binding issues specifically (like in the answer).Blabbermouth
Not "most of the time", all of the time. You could write exactly the same thing in Java or any other widely used imperative language, and it would do the same thing. Try it online!Conclusion
@Conclusion "all of the time" is wrong. Python and Java models are not identical.Blabbermouth
@Blabbermouth Obviously their execution models are not identical, but the semantics for variables, assignments, references, and parameter passing are the same. If you believe otherwise, please show an example where you think they are different.Conclusion
do provide some references for your bold claims. My references are in the answer. More specifically the exact reference for "naming and binding" for Java would be nice.Blabbermouth
This is extremely helpful, but I have a follow up question. @Blabbermouth and anyone else who cares to answer, is modifying arguments considered good practice? I don't remember ever modifying an object within a method, intending to affect it in a broader scope. I've always returned the modified object and reassigned the variable in the broader scope. Was I just being wasteful with that approach? Could returning the object and reassigning it be considered clearer? Is there anything else to consider, or should I just be doing the simplest thing that uses the least code?Haldeman
@Haldeman regarding "returned the modified object": Command-query recommends returning None if you are modifying the object e.g., L.sort() sorts a list inplace and therefore return None (compare with sorted(L) that returns a new sorted list keeping L list unchanged). It is just a pattern e.g., there is Fluent interface where the object is always returned.Blabbermouth
@Haldeman you might find interesting what Guido wrote (2003) about it mail.python.org/pipermail/python-dev/2003-October/038855.html (pro Command-query, against Fluent interface)Blabbermouth
I
28

You've got a number of answers already, and I broadly agree with J.F. Sebastian, but you might find this useful as a shortcut:

Any time you see varname =, you're creating a new name binding within the function's scope. Whatever value varname was bound to before is lost within this scope.

Any time you see varname.foo() you're calling a method on varname. The method may alter varname (e.g. list.append). varname (or, rather, the object that varname names) may exist in more than one scope, and since it's the same object, any changes will be visible in all scopes.

[note that the global keyword creates an exception to the first case]

Invariant answered 22/2, 2009 at 21:52 Comment(0)
M
20

f doesn't actually alter the value of x (which is always the same reference to an instance of a list). Rather, it alters the contents of this list.

In both cases, a copy of a reference is passed to the function. Inside the function,

  • n gets assigned a new value. Only the reference inside the function is modified, not the one outside it.
  • x does not get assigned a new value: neither the reference inside nor outside the function are modified. Instead, x’s value is modified.

Since both the x inside the function and outside it refer to the same value, both see the modification. By contrast, the n inside the function and outside it refer to different values after n was reassigned inside the function.

Minestrone answered 22/2, 2009 at 16:47 Comment(7)
"copy" is misleading. Python doesn't have variables like C. All names in Python are references. You can't modify name, you just can bind it to another object, that's all. It only makes sense to talk about mutable and immutable object in Python not they are names.Blabbermouth
@J.F. Sebastian: Your statement is misleading at best. It is not useful to think of numbers as being references.Hobie
@dysfunctor: numbers are references to immutable objects. If you'd rather think of them some other way, you have a bunch of odd special cases to explain. If you think of them as immutable, there are no special cases.Anamariaanamnesis
@S.Lott: Regardless of what's going on under the hood, Guido van Rossum put a lot of effort into designing Python so that the programmer can thing of numbers as being just ... numbers.Hobie
@J.F., the reference is copied.Woothen
@Aaron: It might or might not be so. Underlying implementation can construct a new object that represents a name in Python and then bind it to corresponding object. C is not the only implementation language. You can implement Python in pure Python.Blabbermouth
@Hobie Regardless, is is something you should know, because the difference exists in python. For small numbers, you typically have the same object, because they are cached. This is not so for large numbers: >>> a = 1e10 >>> b = 1e10 >>> a is b FalseOutgeneral
H
10

I will rename variables to reduce confusion. n -> nf or nmain. x -> xf or xmain:

def f(nf, xf):
    nf = 2
    xf.append(4)
    print 'In f():', nf, xf

def main():
    nmain = 1
    xmain = [0,1,2,3]
    print 'Before:', nmain, xmain
    f(nmain, xmain)
    print 'After: ', nmain, xmain

main()

When you call the function f, the Python runtime makes a copy of xmain and assigns it to xf, and similarly assigns a copy of nmain to nf.

In the case of n, the value that is copied is 1.

In the case of x the value that is copied is not the literal list [0, 1, 2, 3]. It is a reference to that list. xf and xmain are pointing at the same list, so when you modify xf you are also modifying xmain.

If, however, you were to write something like:

    xf = ["foo", "bar"]
    xf.append(4)

you would find that xmain has not changed. This is because, in the line xf = ["foo", "bar"] you have change xf to point to a new list. Any changes you make to this new list will have no effects on the list that xmain still points to.

Hope that helps. :-)

Hobie answered 22/2, 2009 at 17:15 Comment(3)
"In the case of n, the value that is copied..." -- This is wrong, there is no copying done here (unless you count references). Instead, python uses 'names' which point to the actual objects. nf and xf point to nmain and xmain, until nf = 2, where the name nf is changed to point to 2. Numbers are immutable, lists are mutable.Outgeneral
Why is n passed by value but x passed by reference (which your answer implies, non?)?Rubidium
@Rubidium both are passed by reference. For n and nf at the start of f(), the reference is to the number 1, so writing nf = 2 means you are changing nf's reference to point to 2 instead of 1; n in main still points to 1. But the references for x and xf at the start of f() both point to the list [0,1,2,3], so writing xf.append(4) modifies the thing that both xf and x reference and x can also see the changed list, while writing xf = ['foo', 'bar'] modifies xf so that it now references a whole new list ['foo', 'bar'], and x in main still references the original list.Unfair
O
8

If the functions are re-written with completely different variables and we call id on them, it then illustrates the point well. I didn't get this at first and read jfs' post with the great explanation, so I tried to understand/convince myself:

def f(y, z):
    y = 2
    z.append(4)
    print ('In f():             ', id(y), id(z))

def main():
    n = 1
    x = [0,1,2,3]
    print ('Before in main:', n, x,id(n),id(x))
    f(n, x)
    print ('After in main:', n, x,id(n),id(x))

main()
Before in main: 1 [0, 1, 2, 3]   94635800628352 139808499830024
In f():                          94635800628384 139808499830024
After in main: 1 [0, 1, 2, 3, 4] 94635800628352 139808499830024

z and x have the same id. Just different tags for the same underlying structure as the article says.

Obelia answered 30/12, 2017 at 18:0 Comment(2)
Ironically that link doesn't work for meRubidium
Yeap - I suppose after a few years that could happen....Obelia
K
3

It´s because a list is a mutable object. You´re not setting x to the value of [0,1,2,3], you´re defining a label to the object [0,1,2,3].

You should declare your function f() like this:

def f(n, x=None):
    if x is None:
        x = []
    ...
Kurzawa answered 22/2, 2009 at 17:6 Comment(2)
It has nothing to do with mutability. If you would do x = x + [4] instead of x.append(4), you'd see no change in the caller as well although a list is mutable. It has to do with if it is indeed mutated.Sunset
OTOH, if you do x += [4] then x is mutated, just like what happens with x.append(4), so the caller will see the change.Fritzsche
D
3

n is an int (immutable), and a copy is passed to the function, so in the function you are changing the copy.

X is a list (mutable), and a copy of the pointer is passed o the function so x.append(4) changes the contents of the list. However, you you said x = [0,1,2,3,4] in your function, you would not change the contents of x in main().

Disordered answered 22/2, 2009 at 17:7 Comment(4)
Watch the "copy of the pointer" phrasing. Both places get references to the objects. n is a reference to an immutable object; x is a reference to a mutable object.Anamariaanamnesis
The int is not copied. Mutability has nothing to do with how assignment works; what's relevant is that a list has a method that you can call to mutate it.Minacious
@Minacious Does int have no methods that can mutate it? I think you should write an answer as you seem to have a sense of what's going on and you only posted recently..! Doesn't mutability <-> methods that can be called to mutate a variable? What if the mutating method is not called?Rubidium
A mutable object is one that can be mutated. It doesn't mean every method mutates it. Assignment doesn't care about mutability; it's just assigns a name to an object.Minacious
S
3

My general understanding is that any object variable (such as a list or a dict, among others) can be modified through its functions. What I believe you are not able to do is reassign the parameter - i.e., assign it by reference within a callable function.

That is consistent with many other languages.

Run the following short script to see how it works:

def func1(x, l1):
    x = 5
    l1.append("nonsense")

y = 10
list1 = ["meaning"]
func1(y, list1)
print(y)
print(list1)
Sallet answered 21/10, 2018 at 17:40 Comment(2)
There is no such thing as an "object variable". Everything is an object in Python. Some objects expose mutator methods (i.e. they are mutable), others do not.Oregon
Bro the output at the end is missing. What is the result?Immitigable
K
2

Python is a pure pass-by-value language if you think about it the right way. A python variable stores the location of an object in memory. The Python variable does not store the object itself. When you pass a variable to a function, you are passing a copy of the address of the object being pointed to by the variable.

Contrast these two functions

def foo(x):
    x[0] = 5

def goo(x):
    x = []

Now, when you type into the shell

>>> cow = [3,4,5]
>>> foo(cow)
>>> cow
[5,4,5]

Compare this to goo.

>>> cow = [3,4,5]
>>> goo(cow)
>>> goo
[3,4,5]

In the first case, we pass a copy the address of cow to foo and foo modified the state of the object residing there. The object gets modified.

In the second case you pass a copy of the address of cow to goo. Then goo proceeds to change that copy. Effect: none.

I call this the pink house principle. If you make a copy of your address and tell a painter to paint the house at that address pink, you will wind up with a pink house. If you give the painter a copy of your address and tell him to change it to a new address, the address of your house does not change.

The explanation eliminates a lot of confusion. Python passes the addresses variables store by value.

Klaus answered 4/12, 2010 at 14:48 Comment(2)
A pure pass by pointer value is not very different from a pass by reference if you think about it the right way...Cherise
Look at goo. Were you pure pass by reference, it would have changed its argument. No, Python is not a pure pass-by-reference language. It passes references by value.Klaus
S
2

Python is copy by value of reference. An object occupies a field in memory, and a reference is associated with that object, but itself occupies a field in memory. And name/value is associated with a reference. In python function, it always copy the value of the reference, so in your code, n is copied to be a new name, when you assign that, it has a new space in caller stack. But for the list, the name also got copied, but it refer to the same memory(since you never assign the list a new value). That is a magic in python!

Subinfeudation answered 16/1, 2016 at 16:30 Comment(1)
Why are lists and integers handled differently - mutability, or something else?Rubidium
I
1

When you are passing the command n = 2 inside the function, it finds a memory space and label it as 2. But if you call the method append, you are basically refrencing to location x (whatever the value is) and do some operation on that.

Ineducation answered 23/1, 2022 at 10:22 Comment(0)
L
0

As jouell said. It's a matter of what points to what and i'd add that it's also a matter of the difference between what = does and what the .append method does.

  1. When you define n and x in main, you tell them to point at 2 objects, namely 1 and [1,2,3]. That is what = does : it tells what your variable should point to.

  2. When you call the function f(n,x), you tell two new local variables nf and xf to point at the same two objects as n and x.

  3. When you use "something"="anything_new", you change what "something" points to. When you use .append, you change the object itself.

  4. Somehow, even though you gave them the same names, n in the main() and the n in f() are not the same entity, they only originally point to the same object (same goes for x actually). A change to what one of them points to won't affect the other. However, if you instead make a change to the object itself, that will affect both variables as they both point to this same, now modified, object.

Lets illustrate the difference between the method .append and the = without defining a new function :

compare

    m = [1,2,3]
    n = m   # this tells n to point at the same object as m does at the moment
    m = [1,2,3,4] # writing m = m + [4] would also do the same
    print('n = ', n,'m = ',m)

to

    m = [1,2,3]
    n = m
    m.append(4)
    print('n = ', n,'m = ',m)

In the first code, it will print n = [1, 2, 3] m = [1, 2, 3, 4], since in the 3rd line, you didnt change the object [1,2,3], but rather you told m to point to a new, different, object (using '='), while n still pointed at the original object.

In the second code, it will print n = [1, 2, 3, 4] m = [1, 2, 3, 4]. This is because here both m and n still point to the same object throughout the code, but you modified the object itself (that m is pointing to) using the .append method... Note that the result of the second code will be the same regardless of wether you write m.append(4) or n.append(4) on the 3rd line.

Once you understand that, the only confusion that remains is really to understand that, as I said, the n and x inside your f() function and the ones in your main() are NOT the same, they only initially point to the same object when you call f().

Locksmith answered 6/1, 2022 at 22:29 Comment(0)
U
-2

Please allow me to edit again. These concepts are my experience from learning python by try error and internet, mostly stackoverflow. There are mistakes and there are helps.

Python variables use references, I think reference as relation links from name, memory adress and value.

When we do B = A, we actually create a nickname of A, and now the A has 2 names, A and B. When we call B, we actually are calling the A. we create a ink to the value of other variable, instead of create a new same value, this is what we call reference. And this thought would lead to 2 porblems.

when we do

A = [1]
B = A   # Now B is an alias of A

A.append(2)  # Now the value of A had been changes
print(B)
>>> [1, 2]  
# B is still an alias of A
# Which means when we call B, the real name we are calling is A

# When we do something to B,  the real name of our object is A
B.append(3)
print(A)
>>> [1, 2, 3]

This is what happens when we pass arguments to functions

def test(B):
    print('My name is B')
    print(f'My value is {B}') 
    print(' I am just a nickname,  My real name is A')
    B.append(2)


A = [1]
test(A) 
print(A)
>>> [1, 2]

We pass A as an argument of a function, but the name of this argument in that function is B. Same one with different names.
So when we do B.append, we are doing A.append When we pass an argument to a function, we are not passing a variable , we are passing an alias.

And here comes the 2 problems.

  1. the equal sign always creates a new name
A = [1]
B = A
B.append(2)
A = A[0]  # Now the A is a brand new name, and has nothing todo with the old A from now on.

B.append(3)
print(A)
>>> 1
# the relation of A and B is removed when we assign the name A to something else
# Now B is a independent variable of hisown.

the Equal sign is a statesment of clear brand new name,

this was the concused part of mine

 A = [1, 2, 3]

# No equal sign, we are working on the origial object,
A.append(4)
>>> [1, 2, 3, 4]

# This would create a new A
A = A + [4]  
>>> [1, 2, 3, 4]

and the function

def test(B):
    B = [1, 2, 3]   # B is a new name now, not an alias of A anymore
    B.append(4)  # so this operation won't effect A
    
A = [1, 2, 3]
test(A)
print(A)
>>> [1, 2, 3]

# ---------------------------

def test(B):
    B.append(4)  # B is a nickname of A, we are doing A
    
A = [1, 2, 3]
test(A)
print(A)
>>> [1, 2, 3, 4]

the first problem is

  1. the left side of and equation is always a brand new name, new variable,

  2. unless the right side is a name, like B = A, this create an alias only

The second problem, there are something would never be changed, we cannot modify the original, can only create a new one.

This is what we call immutable.

When we do A= 123 , we create a dict which contains name, value, and adress.

When we do B = A, we copy the adress and value from A to B, all operation to B effect the same adress of the value of A.

When it comes to string, numbers, and tuple. the pair of value and adress could never be change. When we put a str to some adress, it was locked right away, the result of all modifications would be put into other adress.

A = 'string' would create a protected value and adess to storage the string 'string' . Currently, there is no built-in functions or method cound modify a string with the syntax like list.append, because this code modify the original value of a adress.

the value and adress of a string, a number, or a tuple is protected, locked, immutable.

All we can work on a string is by the syntax of A = B.method , we have to create a new name to storage the new string value.

please extend this discussion if you still get confused. this discussion help me to figure out mutable / immutable / refetence / argument / variable / name once for all, hopely this could do some help to someone too.

##############################

had modified my answer tons of times and realized i don't have to say anything, python had explained itself already.

a = 'string'
a.replace('t', '_')
print(a)
>>> 'string'

a = a.replace('t', '_')
print(a)
>>> 's_ring'

b = 100
b + 1
print(b)
>>> 100

b = b + 1
print(b)
>>> 101

def test_id(arg):
    c = id(arg)
    arg = 123
    d = id(arg)
    return

a = 'test ids'
b = id(a)
test_id(a)
e = id(a)

# b = c  = e != d
# this function do change original value
del change_like_mutable(arg):
    arg.append(1)
    arg.insert(0, 9)
    arg.remove(2)
    return
 
test_1 = [1, 2, 3]
change_like_mutable(test_1)



# this function doesn't 
def wont_change_like_str(arg):
     arg = [1, 2, 3]
     return


test_2 = [1, 1, 1]
wont_change_like_str(test_2)
print("Doesn't change like a imutable", test_2)

This devil is not the reference / value / mutable or not / instance, name space or variable / list or str, IT IS THE SYNTAX, EQUAL SIGN.

Uriniferous answered 8/1, 2020 at 3:23 Comment(1)
Maybe you can understand what is happening just seeing the code, but not why, and @Brant wants to understand why, not what.Encomiastic

© 2022 - 2024 — McMap. All rights reserved.