1

In Python, it's possible to alter some types of objects within a function without having to return anything. For some objects, this is not the case.

For instance, if we pass a dictionary to a function and alter it within the function, the original dictionary will be altered:

def add_key(d, key):
    d[key] = True

d = {}
print d
# {}

add_key(d, 1)
print d
# {1: True}

In contrast, we can't alter an integer like this:

def increment_int(i):
    i += 1

i = 1
print i
# 1

increment_int(i)
print i
# still 1!

At first I thought that maybe the rule is simply that immutable objects cannot be altered within a function in this way. A similar example does not work with strings:

def append_string(s1, s2):
    s1 += s2

s = "hello"
print s
# "hello"

append_string(s, " world")
print s
# "hello", not "hello world" :( 

I'm confused though, because it seems that certain operations applied to a mutable object within a function will be visible afterwards, while others wont. E.g. appending an item to a list does work:

def append_list(l, val):
    l.append(val)

l = []
print l
# []

append_list(l, 1)
print l
# [1]

while swapping the names of two lists doesn't:

def swap_lists(l1, l2):
    l1, l2 = l2, l1

l1 = [1]
l2 = [2]
print l1, l2
# [1] [2]

swap_lists(l1, l2)
print l1, l2
# still [1] [2], even though lists are mutable!

Yet another example, with seemingly weird and unexpected behaviour:

def swap_then_append(l1, l2):
    l1, l2 = l2, l1
    l1.append(1)
    l2.append(2)

l1 = []
l2 = []
print l1, l2
# [] []

swap_then_append(l1, l2)
print l1, l2
# [2] [1]

Can someone explain what is happening here?


Edit: a possible duplicate of this question has been identified; the answers there partially explain to me what is going on, I still don't get why, for instance, the function swap_lists above doesn't actually swap the lists. And now that I am thinking about it further, I still don't actually understand why increment_int and append_string above don't do what I initially expected, other than that it has something to do with the immutability of integers and strings.

Paul Rubenstein
  • 342
  • 1
  • 2
  • 12
  • Thanks for pointing out the possible duplicate - it was helpful to read that question and the answers, but I still don't feel that I understand what is happening here. I've added an edit at the bottom acknowledging the possible duplicate, but I'd still very much appreciate a more in depth answer from someone! – Paul Rubenstein Nov 02 '17 at 00:45
  • 3
    You can mutate mutable objects, but assigning (which includes swapping) binds a new object to the name, so its effects are local to the scope containing that name. Please see [Facts and myths about Python names and values](http://nedbatchelder.com/text/names.html) – PM 2Ring Nov 02 '17 at 00:55
  • 2
    A "variable" in Python is better thought of as just as label for an object. The object exists with or without any labels attached to it (well, sort of--if the object isn't referred to by any other object then it is destroyed but that's an aside). Point being the object exists independently of the label or labels attached to it. For example `l1 = l2 = [1, 2, 3]` creates two labels stuck on the same list. This has nothing to do with mutability or immutability of the object. When one label is assigned to one object, and then you assign it to a different object, you're just swapping that label. – Iguananaut Nov 02 '17 at 01:01
  • Also see [Other languages have variables, Python has names](http://python.net/~goodger/projects/pycon/2007/idiomatic/handout.html#other-languages-have-variables) – PM 2Ring Nov 02 '17 at 01:03

1 Answers1

3

The explanations in the dupe target explain what's going on pretty well, but I'll address what happens in swap_then_append.

def swap_then_append(l1, l2):
    print l1,l2
    # [3],[4]
    l1, l2 = l2, l1
    print l1,l2
    # [4] [3]
    l1.append(1)
    l2.append(2)

l1 = [3]
l2 = [4]
print l1, l2
# [3] [4]

swap_then_append(l1, l2)
print l1, l2
# [3,2] [4,1]

The 'swapping' changes the names of the variables used to refer to the lists, but only within the function. While mutation of the underlying object is permanent, the assignment of variables depends on the scope. The new l1 and l2 in the function might as well be called something else, as they are now just shadowing the original variables l1 and l2 defined in the function signature, which, in turn, shadow the l1 and l2 defined in the main script. The mutations then apply to the original lists.

yinnonsanders
  • 1,831
  • 11
  • 28