I know that one has to be careful with mutable arguments like lists applied to function calls, so that things like this won't accidentally happen:

def add_item(list2):
    list2.append(99)
    return list2
 
list1 = [1, 2, 3]
list3 = add_item(list1)
 
print list1  # [1, 2, 3, 99]  oops!
print list3  # [1, 2, 3, 99]  expected

Can somebody explain to me why exactly why this happens? Also, can this be usefull?

Recommended Answers

All 6 Replies

if you don't want to change list1, you can do

list1dup = list1[:]
list3 = add_item(list1dup)
...

I know that one has to be careful with mutable arguments like lists applied to function calls, so that things like this won't accidentally happen:

def add_item(list2):
    list2.append(99)
    return list2
 
list1 = [1, 2, 3]
list3 = add_item(list1)
 
print list1  # [1, 2, 3, 99]  oops!
print list3  # [1, 2, 3, 99]  expected

Can somebody explain to me why exactly why this happens? Also, can this be usefull?

Python passes arguments by reference. If you change an immutable object like a number, tuple or a string that object simply receives a new address, nothing feeds back to the caller. This is different with a mutable object like a list. If you only change the elements in the list, then the reference of the object itself has not changed. In your case list2 is just an alias of list1 and shares the same reference. Now you are feeding back to the caller!

To illustrate that, we can use Ghostdog's suggestion on avoiding the alias by making a true copy of list1/list2 in the function. So in the function's first line use list2 = list(list2) and see what happens now.

One use would be to count how many times a function has been called ...

def counter4(count_list=[0]):
    """
    store the current count in the first element of count_list
    behaves like a static variable in C
    default count_list=[0] --> starts with zero
    """
    # your code here ...
    
    # now update the counter
    count_list[0] += 1
    return count_list[0]
# test it
print counter4()  # 1
print counter4()  # 2
print counter4()  # 3
print counter4()  # 4

Vegaseat's explanation will take a while to sink in, but I am seeing the light. Changed my sample code to:

def add_item(list2):
    # make a true copy first
    list2 = list(list2)
    list2.append(99)
    return list2
list1 = [1, 2, 3]
list3 = add_item(list1)
print list1  # [1, 2, 3]  works now!
print list3  # [1, 2, 3, 99]  expected

The thing with counter4() I don't quite get. It works in an interesting way. I am not passing a list to it, but I guess the default does. Does that mean that a default argument only gets evaluated on the first call to the function? Otherwise it would reset to zero all the time.

The thing with counter4() I don't quite get. It works in an interesting way. I am not passing a list to it, but I guess the default does. Does that mean that a default argument only gets evaluated on the first call to the function? Otherwise it would reset to zero all the time.

Your hunch is correct, Python makes the function an object only with the first call to it. If the default is a list then the reference to the list object is maintained and therefor any items stored in it are updated and available on the next call. Something to remember.

I find counter4 to be a very interesting function, thanks for putting it up vegaseat.

@vega

Here was one interesting feature of counter4():

def counter4(count_list=[0]):
    """
    store the current count in the first element of count_list
    behaves like a static variable in C
    default count_list=[0] --> starts with zero
    """
    # your code here ...
    
    # now update the counter
    count_list[0] += 1
    return count_list[0]

>>> for i in range(5):
    print counter4()

    
1
2
3
4
5
>>> counter4([1,2,3])
2
>>> counter4()
6    <----   !!!  Ahh...
>>>

That last was really surprising; I was expecting the counter to be reset -- but it wasn't. And then it all made sense: the default argument isn't the list; it's the pointer to the list. Ah, clarity.

Thanks, vega.

@Sneekula
It's actually a Good Thing that Python assigns and passes objects by reference instead of by value. Imagine if you wanted to do this:

def reverse(mylist):
   def reverse(mylist):
      for x in range(1, len(mylist)/2):
        mylist[x-1],mylist[-x] = mylist[-x],mylist[x-1]

>>> ml = [1,2,3,4,5,6]
>>> reverse(ml)     # <-----
>>> ml
[6,5,4,3,2,1]

(Of course, this just duplicates the reverse method that exists already for lists.) Now, suppose at the indicated line that ml is passed as a list, instead of as a reference. Then reverse.mylist becomes ml, and reverse() indeed reverses it.

But now something bad happens: reverse returns, and mylist goes out of scope. All of the hard work is lost!

But because Python passes a reference (technically: a copy of the reference, I think), then mylist points to the same object that ml points to. As a result, any changes made to mylist get made simultaneously to ml.

Hence, passing references around makes it possible to have methods that actually make real changes to mutable objects.

The need for something like this is why C programs routinely pass pointers and use the -> operator, rather than passing entire structures and using the . operator.

That, and it's also more efficient! Imagine the memory requirements and slow performance if Python put entire objects like, say, image objects on the stack every time a function were called. Much better to pass a 4-byte pointer. :)

Jeff

Be a part of the DaniWeb community

We're a friendly, industry-focused community of developers, IT pros, digital marketers, and technology enthusiasts meeting, networking, learning, and sharing knowledge.