Modifying a list in a Python function - python

I am attempting a variant of this simple function:
def reverse_this(thisArray):
saved_first = thisArray[0]
thisArray = thisArray[1:]
thisArray.reverse()
thisArray.insert(0,saved_first)
myList = ['foo', 1,2,3,4,5]
reverse_this(myList)
print('after:', myList)
In essence, I want it to print out "after: ['foo', 5, 4, 3, 2, 1]". However, the slicing here seems to be causing some issues. It appears that the list is being modified as intended inside the function, however, the changes are not being applied outside the function to myList. What's going on here and how can I fix it?

Python is pass by assignment for passing arguments inside the function. When you do
thisArray = thisArray[1:]
inside your function, you re-bind the passed argument to a new object created inside your function and proceed to mutate that copy created after slicing instead, rather than modifying your argument thisArray. To get the modified value, one way is to return this modified list from your function:
def reverse_this(thisArray):
saved_first = thisArray[0]
thisArray = thisArray[1:]
thisArray.reverse()
thisArray.insert(0,saved_first)
return thisArray
myList = ['foo', 1,2,3,4,5]
myList = reverse_this(myList)
print('after:', myList)
Other way is to modify the list's contents (assign to the slice) rather than re-binding the list itself:
def reverse_this(thisArray):
saved_first = thisArray[0]
thisArray[:] = thisArray[1:] # assign to the sliced list's contents
thisArray.reverse()
thisArray.insert(0,saved_first)

thisArray is re-assigned to a new list object by slicing, so any changes after that don't affect the original list passed into the function. Note the instance IDs of the lists:
def reverse_this(thisArray):
saved_first = thisArray[0]
print(f'thisArray pre-slice: {id(thisArray):#x}')
thisArray = thisArray[1:]
print(f'thisArray post-slice: {id(thisArray):#x}')
thisArray.reverse()
thisArray.insert(0,saved_first)
myList = ['foo', 1,2,3,4,5]
print(f'myList pre-call: {id(myList):#x}')
reverse_this(myList)
print(f'myList post-call: {id(myList):#x}')
print('after:', myList)
myList pre-call: 0x1e2f5713500
thisArray pre-slice: 0x1e2f5713500 # parameter refers to original list
thisArray post-slice: 0x1e2f5753800 # thisArray refers to new list
myList post-call: 0x1e2f5713500 # original list isn't changed.
after: ['foo', 1, 2, 3, 4, 5]
In Python, variables are names of objects. If you mutate an object, all names of that object "see" the change. A slice makes a new object, and in this case the name was reassigned to the new object.
To fix it, assign the slice into the full range of the original list, which mutates the original list instead of creating a new one:
def reverse_this(thisArray):
saved_first = thisArray[0]
thisArray[:] = thisArray[1:] # replace entire content of original list with slice.
thisArray.reverse() # thisArray still refers to original list here.
thisArray.insert(0,saved_first)
myList = ['foo', 1,2,3,4,5]
reverse_this(myList)
print('after:', myList)
after: ['foo', 5, 4, 3, 2, 1]
When assigning to a slice, a section of a list is replaced in place with another list. It doesn't necessarily have to be the same size.
Another example:
>>> s = list(range(10))
>>> s
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> id(s)
2339680033664
>>> s[4:7] = [10,10] # replace indices 4 up to but not including 7
>>> s
[0, 1, 2, 3, 10, 10, 7, 8, 9]
>>> id(s) # id didn't change
2339680033664

You can do the following to achieve the same thing:
def my_reverse(lst):
lst[1:] = reversed(lst[1:]) # Or lst[:0:-1]
Assigning to a list slice does not create any copies it simply replaces that slice.
See this.

Related

Why python call by reference is so unruly

arr = [1]
def f1(lst):
lst.append(2)
print(lst)
lst = 2
print(lst)
f1(arr)
print(arr) # [1,2]
why python call by reference parameter does not change to value?
what does lst variable indeicating when do "lst = 2"
(not connected to arr?)
Assigning lst = 2 doesn't affect the value of arr. In fact, python doesn't do "call by reference" at all.
Annotating your code with comments that might help clear it up:
arr = [1]
def f1(lst):
# lst and arr both refer to the same [1] list at this point.
# Two different and independent names for the same object.
lst.append(2) # appends 2 to [1], aka lst, aka arr
print(lst) # lst/arr is now [1, 2]
lst = 2 # reassign the name 'lst' to the value 2!
# At this point, lst refers to 2 instead of [1, 2].
# lst and arr are no longer connected.
# arr is still [1, 2] even though lst is 2.
print(lst) # indeed, lst is now 2
# but arr is still [1, 2], as seen below:
f1(arr)
print(arr) # [1,2]
https://nedbatchelder.com/text/names.html is highly recommended reading on this topic! The main thing to understand is that lst and arr are just different names that at different points in the code might refer to the same value or different values.
When you call lst.append, you are modifying the value that lst is a name for, which arr also happens to be a name for. When you say lst = 2, you are rebinding the name lst, but you are not modifying the value that it previously referred to (and to which arr still refers).

Changing the passed-in list within a function [duplicate]

This question already has answers here:
Passing values in Python [duplicate]
(8 answers)
Closed 2 years ago.
Consider the Python code below:
def change_list(in_list):
in_list = [1,2,3]
def change_list_append(in_list):
in_list.append(7)
my_list = [1,2,3,4,5,6]
change_list(my_list)
print(my_list)
change_list_append(my_list)
print(my_list)
The output is:
[1, 2, 3, 4, 5, 6]
[1, 2, 3, 4, 5, 6, 7]
I don't understand why the first call (change_list) did not result in my_list to become [1,2,3]. Can someone explain? Look at what happens with the call to change_list_append: number 7 is appended to my_list.
Assigning to a name never changes the value previously referenced by the list. This is true even without invoking function parameters.
>>> x = [1,2,3]
>>> y = x
>>> y = [4,5,6]
>>> x
[1, 2, 3]
Changing what y refers to does not alter what x refers to.
In your second example, you are invoking a mutating method on the list referenced by the parameter. Both my_list and in_list refer to the same list, so it any changes made to the list via either reference are visible from either reference.
See https://nedbatchelder.com/text/names.html for a more in-depth discussion of how references work in Python.
I think this post sums it up nicely => Why can a function modify some arguments as perceived by the caller, but not others?
You can assign items to the in_list using methods (like the .clear() method or the .assign() ) not rename.
As mentioned by others, the 1st function does not modify the global my_list. Instead it modifies the local variable
If you want change_list to modify the global variable you need to the function to return the new value and assign it to my_list.
Something like this will do:
def change_list():
newList = [1,2,3]
return newList
def change_list_append(in_list):
in_list.append(7)
my_list = [1,2,3,4,5,6]
my_list = change_list()
print(my_list)
change_list_append(my_list)
print(my_list)
Output:
[1, 2, 3]
[1, 2, 3, 7]

Python List Mutation Doesn't Happen When Variable Reassigned. Why?

What strange magic is this?
def rotate_list(lst, n):
n = n % len(lst)
lst = lst[-n:] + lst[:-n]
def rotate_list_2(lst):
lst[0], lst[1], lst[2], lst[3] = lst[3], lst[0], lst[1], lst[2]
s1 = [1, 2, 5, 4]
rotate_list(s1, 1)
print(s1)
s1 = [1, 2, 5, 4]
rotate_list_2(s1)
print(s1)
Output:
[1, 2, 5, 4]
[4, 1, 2, 5]
It appears that although lists are generally mutable within functions, if a list with the same name is created, then the original list is unaffected by changes to the new list. Could someone please explain what is happening here please, in terms of scope and references?
How would I rotate the original list without having to manually update each value as in rotate_list_2()? Or would this kind of thing generally be done by working with new lists returned from a function?
Assigning to list in function doesn't change the original reference.
The assignment just references the local parameter lst on the new value.
The original list referenced outside ('before') the function remains intact.
Insead assign to it's elements with this syntax:
def rotate_list(lst, n):
n = n % len(lst)
lst[:] = lst[-n:] + lst[:-n]
s1 = [1, 2, 5, 4]
rotate_list(s1, 1)
# And it works like magic! :)
# [4, 1, 2, 5]
print(s1)
If you reassign the argument of a function, the value does not change outside of the function scope.
def test0(a):
a = 10
print(a)
x = 4
test0(x)
print(x)
This would result in
10
4
The reason why assigning values of an array works is that you're not assigning a new value to the argument itself. You're instead accessing the memory that the array reads from, and you're changing it. Thus, those changes will happen even for outer scopes.
After change in the function you can return the list and capture it in function call like s=function(s)

Function which removes the first item in a list (Python)

I am trying to write a function which removes the first item in a Python list. This is what I've tried. Why doesn't remove_first_wrong change l when I call the function on it? And why does the list slicing approach work when I do it in the main function?
def remove_first_wrong(lst):
lst = lst[1:]
def remove_first_right(lst):
lst.pop(0)
if __name__ == '__main__':
l = [1, 2, 3, 4, 5]
remove_first_wrong(l)
print(l)
l_2 = [1, 2, 3, 4, 5]
remove_first_right(l_2)
print(l_2)
# Why does this work and remove_first_wrong doesn't?
l_3 = [1, 2, 3, 4, 5]
l_3 = l_3[1:]
print(l_3)
Slicing a list returns a new list object, which is a copy of the original list indices you indicated in the slice. You then rebound lst (a local name in the function) to reference that new list instead. The old list is never altered in that process.
list.pop() on the other hand, operates on the list object itself. It doesn't matter what reference you used to reach the list.
You'd see the same thing without functions:
>>> a = [1, 2]
>>> b = a[:] # slice with all the elements, produces a *copy*
>>> b
[1, 2]
>>> a.pop() # remove an element from a won't change b
2
>>> b
[1, 2]
>>> a
[1]
Using [:] is one of two ways of making a shallow copy of a list, see How to clone or copy a list?
You may want to read or watch Ned Batchelder's Names and Values presestation, to further help understand how Python names and objects work.
Inside the function remove_first_wrong the = sign reassigns the name lst to the object on the right. Which is a brand new object, created by slicing operation lst[1:]. Thus, the object lst assigned to is local to that function (and it actually will disappear on return).
That is what Martijn means by "You then rebound lst (a local name in the function) to reference that new list instead."
On contrary, lst.pop(0) is a call to the given object -- it operates on the object.
For example, this will work right too:
def remove_first_right2(lst):
x = lst # x is assigned to the same object as lst
x.pop(0) # pop the item from the object
Alternately, you can use del keyword:
def remove_first_element(lst):
del lst[0]
return lst

Are there efficiency differences in extend vs. adding vs. appending in Python?

I refer to list operations:
L = myList + otherList
L = myList.append([5])
L = myList.extend(otherList)
I am curious if there are efficiency differences among these operations.
These are totally different operations.
They have different purposes, so efficiency wouldn't matter. append is used to append a single value to a list, extend is used for multiple values, and the addition is for when you don't want to modify the original list, but to have another list with the extra values added on.
>>> lst = [1, 2, 3]
>>> lst2 = [5, 6]
>>> lst.append(4) # appending
>>> lst
[1, 2, 3, 4]
>>> lst.extend(lst2) # extending
>>> lst
[1, 2, 3, 4, 5, 6]
>>> lst + lst2 # addition
[1, 2, 3, 4, 5, 6, 5, 6]
Also note that list.append and list.extend operate in-place, so assigning the result to a variable will make that variable hold the value None.
Your example here is sort of misleading in the case of append.
>>> l1 = [1,2,3,4]
>>> l1.append([5])
>>> l1
[1, 2, 3, 4, [5]]
Append takes a single item and appends it to the end of the existing list. By passing in an iterable to append, you're adding another list (in this case) within a list.
extend takes an iterable and essentially calls append for each item in the iterable`, adding the items onto the end of the existing list.
The mylist + otherlist is the only interesting case here, as the result of using the + operator creates a new list, using more memory.
Timing them answers your question about efficiency in regards of speed:
import timeit
def first():
mylist + otherlist
def second():
mylist.append(otherlist)
def third():
mylist.extend(otherlist)
for test in (first, second, third):
mylist = [1, 2, 3, 4]
otherlist = [5]
print "%s: %f" % (test, timeit.timeit(test, number=1000000))
On my machine the result was:
<function first at 0x10ff3ba28>: 0.320835
<function second at 0x10ff3baa0>: 0.275077
<function third at 0x10ff3bb18>: 0.284508
Showing that the first example was clearly slowest.

Categories

Resources