Sihan Zheng
Sihan Zheng

Reputation: 475

Python 3 odd function behavior on list

So I am working with lists in python 3.3, and here is my example code:

def change_to_z(lis):
    lis[3] = 'z'

def change_to_k(lis):
    lis[4] = 'k'


def split(lis):
    lis = lis[3:] + lis[:3]

totest = ['a', 'b', 'c', 'd', 'e', 'f']

change_to_z(totest)
print(totest)
change_to_k(totest)
print(totest)
split(totest)
print(totest)

and the output:

['a', 'b', 'c', 'z', 'e', 'f']
['a', 'b', 'c', 'z', 'k', 'f']
['a', 'b', 'c', 'z', 'k', 'f']

Notice how when I called the first two functions, I was able modify the list, while totest always referred to the list, even as it was changed.

However, with the third function, the variable totest no longer refers to the latest modified version of the list. My debugger tells me that within the function "split", the list is flipped, but outside of the function, it is not flipped. Why is it that the variable name no longer refers to the list?

Why does this happen? And with what operators does this happen? Why is it sometimes that the variable name still refers to the list ever after it is modified in a function, but it doesn't behave that way with other operators?

Upvotes: 4

Views: 176

Answers (3)

bterwijn
bterwijn

Reputation: 289

You can use module memory_graph:

https://pypi.org/project/memory-graph/

to graph your data and easily see what data is shared between variables to better understand why totest isn't changed. Just printing data will not show you what data is shared.

import memory_graph as mg

def change_to_z(lis):
    lis[3] = 'z'

def change_to_k(lis):
    lis[4] = 'k'

def split(lis):
    mg.s() # graph call stack
    lis = lis[3:] + lis[:3] # <--- assigns 'lis' a new list
    mg.s() # graph call stack

totest = ['a', 'b', 'c', 'd', 'e', 'f']

change_to_z(totest)
print(totest)
change_to_k(totest)
print(totest)
split(totest)
print(totest)

When calling split(lis), first the lis variable in the split() function and the totest variable share their data as can be seen in graph1:

enter image description here

But then you assign a new value to lis, this makes the lis variable reference a new list but this does not change the totest variable as can be seen in graph2:

enter image description here

There is no easy way to use call-by-reference in Python in a manner in which split() will assign the new list also to totest, so better look for a different approach. But if you really need to, you can wrap totest in another list and use the [0] index to access it everywhere. The code gets hard to read now, so this is not advised!

import memory_graph as mg

def change_to_z(lis):
    lis[0][3] = 'z'

def change_to_k(lis):
    lis[0][4] = 'k'

def split(lis):
    mg.s() # graph call stack
    lis[0] = lis[0][3:] + lis[0][:3] # <--- assigns 'lis[0]' a new list
    mg.s() # graph call stack
    
totest = [ ['a', 'b', 'c', 'd', 'e', 'f'] ] # <--- wrap in another list

change_to_z(totest)
print(totest[0])
change_to_k(totest)
print(totest[0])
split(totest)
print(totest[0])

If we graph the execution of this program we first see:

enter image description here

and after the assignment we see:

enter image description here

and the totest variable is now updated as desired.

Full disclosure: I am the developer of memory_graph.

Upvotes: 0

Raymond Hettinger
Raymond Hettinger

Reputation: 226336

The assignment to lis is a local variable and is not updating the list. That is easy to see in the python tutor visualization of your program.

Also, see this blog post for a clear explanation of what is going on. Here's a short recap:

  • Names refer to values.
  • Many names can refer to one value.
  • Names are reassigned independently of other names.
  • Assignment never copies data.
  • Changes in a value are visible through all of its names.
  • Python assigns mutable and immutable values differently.
  • References can be more than just names.

In your code, use a global declaration to see how it works different than the default local assignment:

def split(lis):
    global lis

    lis = lis[3:] + lis[:3]

Upvotes: 1

shad0w_wa1k3r
shad0w_wa1k3r

Reputation: 13372

You are wrong about scope of the function.

If you really want to change the supplied list, you can try out of one the following 2 methods.

def split(lis):
    global lis
    lis = lis[3:] + lis[:3]

Or better

def split(lis):
    lis[:] = lis[3:] + lis[:3] # Credits go to drewk for pointing this out

When you are doing

def split(lis):
    lis = lis[3:] + lis[:3]

In the split function, you are creating a local list with the same identifier as that of the supplied list. But, these two won't clash since passed list is global.

If you want to play around & know if they are really different, use id()

lis = [1, 2, 3]
def split(lis):
    lis = lis[3:] + lis[:3]
    print id(lis)
split(lis)
print id(lis)

Output of the above code

40785920
40785600

Notice how the memory locations are different

Upvotes: 2

Related Questions