I have a situation in which the value of the default argument in a function head is affected by an if-clause within the function body. I am using Python 3.7.3.
function definitions
We have two functions f and j. I understand the behavior of the first two function. I don't understand the second function's behavior.
def f(L=[]):
print('f, inside: ', L)
L.append(5)
return L
def j(L=[]):
print('j, before if:', L)
if L == []:
L = []
print('j, after if: ', L)
L.append(5)
return L
function behavior that I understand
Now we call the first function three times:
>>> print('f, return: ', f())
f, inside: []
f, return: [5]
>>> print('f, return: ', f())
f, inside: [5]
f, return: [5, 5]
>>> print('f, return: ', f())
f, inside: [5, 5]
f, return: [5, 5, 5]
The empty list [] is initialized on the first call of the function. When we append 5 to L then the one instance of the list in the memory is modified. Hence, on the second function call, this modified list is assigned to L. Sounds reasonable.
function behavior that I don't unterstand
Now, we call the third function (j) and get:
>>> print('j, return: ', j())
j, before if: []
j, after if: []
j, return: [5]
>>> print('j, return: ', j())
j, before if: []
j, after if: []
j, return: [5]
>>> print('j, return: ', j())
j, before if: []
j, after if: []
j, return: [5]
According to the output, L is an empty list in the beginning of each call of the function j. When I remove the if-clause, in the body of function j the function is equal to function f and yields the same output. Hence, the if-clause seems to have some side effect. Testing for len(L) == 0 instead of L == [] in j has the same effect.
related to
My question is related to:
Stackoverflow Question Python Default Arguments Evaluation
Stackoverflow Question "Least Astonishment" and the Mutable Default Argument and
the Python Tutorial Section 4.7.1. (Default Argument Values)
But the answers to these question and the tutorial answer only, what I already know.
Modify your print statements to also print id(L):
def j(L=[]):
print('j, before if:', L, id(L))
if L == []:
L = []
print('j, after if: ', L, id(L))
L.append(5)
return L
Now check your results:
>>> j()
j, before if: [] 2844163925576
j, after if: [] 2844163967688
[5]
Note the difference in the IDs. By the time you get to the portion of the function where you modify L, it no longer refers to the same object as the default argument. You've rebound L to a new list (with the L = [] in the body of the if statement), and as a result, you are never changing the default argument.
if all boils down to this:
if L == [5]:
L = []
Here you're not changing the value of the default argument, just binding locally to a new name.
You can do that instead:
if L == [5]:
L.clear()
or
if L == [5]:
L[:] = []
to clear the data of the parameter object.
def a():
print "assigning default value"
return []
def b(x=a()):
x.append(10)
print "inside b",x
return x
print b()
print b()
print b()
try running this. You'll see that default value is not assigned every time you run the function
OUTPUT
assigning default value
inside b [10]
[10]
inside b [10, 10]
[10, 10]
inside b [10, 10, 10]
[10, 10, 10]
only once it called the 'a' function to the default value. rest is very well explained above about the compilation of a method so not repeating the same
Related
I have tried the following in the console:
>>> def f(l=[]):
... l.append(1)
... print(l)
... del l
...
>>> f()
[1]
>>> f()
[1, 1]
What I don't understand is how the interpreter is still able to find the same list l after the delete instruction.
From the documentation l=[] should be evaluated only once.
The variable is not the object. Each time the function is called, the local variable l is created and (if necessary) set to the default value.
The object [], which is the default value for l, is created when the function is defined, but the variable l is created each time the function runs.
To delete a element for the list,del l[:] should be used.If u use just l the list will remain itself.
def f(l=[]):
l.append(1)
print(l)
del l[:]
print(l)
>>> f()
[1] #element in the list
[] #list after deletion of the element
>>> f()
[1]
[]
>>> f()
[1]
[]
Here are two pieces of codes which were under the standard of python3.6. And they are the examples in the docs of python3.6(tutorial, page25).
The first is:
def f(a, L=[]):
L.append(a)
return L
print(f(1))
print(f(2))
print(f(3))
the result:
[1]
[1, 2]
[1, 2, 3]
the second:
def f(a, L = None):
if L is None:
L = []
L.append(a)
return L
print(f(1))
print(f(2))
print(f(3))
the result:
[1]
[2]
[3]
So, in the second piece of code, i am confused that after print(f(1)) was executed, print(f(2)) would pass a = 2 and L=[1] to the f(), but why f() didn't get the L=[1]?
If L = None in the second piece of code defines the L to None every time when the f() was called, but why L = [] in the first piece of code don't define L to []
Those two examples show how default arguments work behind the scenes:
the first one demostrates that default arguments 'live' inside the function definition. Meaning: that the value for L in the first function will only ever be reset if you overwrite the whole function with a def section.
The Same is true for the second implementation BUT since it's None: you have to initialize it while the function body is executed. This leads to a fresh list every time the function is called.
This behaviour can be confusing and lead to strange results which is why i heard from most sources that it is best to avoid the first option and work with None default args.
Hope i could clear things up a bit.
In python objects such as lists are passed by reference. Assignment with the = operator assigns by reference. So this function:
def modify_list(A):
A = [1,2,3,4]
Takes a reference to list and labels it A, but then sets the local variable A to a new reference; the list passed by the calling scope is not modified.
test = []
modify_list(test)
print(test)
prints []
However I could do this:
def modify_list(A):
A += [1,2,3,4]
test = []
modify_list(test)
print(test)
Prints [1,2,3,4]
How can I assign a list passed by reference to contain the values of another list? What I am looking for is something functionally equivelant to the following, but simpler:
def modify_list(A):
list_values = [1,2,3,4]
for i in range(min(len(A), len(list_values))):
A[i] = list_values[i]
for i in range(len(list_values), len(A)):
del A[i]
for i in range(len(A), len(list_values)):
A += [list_values[i]]
And yes, I know that this is not a good way to do <whatever I want to do>, I am just asking out of curiosity not necessity.
You can do a slice assignment:
>>> def mod_list(A, new_A):
... A[:]=new_A
...
>>> liA=[1,2,3]
>>> new=[3,4,5,6,7]
>>> mod_list(liA, new)
>>> liA
[3, 4, 5, 6, 7]
The simplest solution is to use:
def modify_list(A):
A[::] = [1, 2, 3, 4]
To overwrite the contents of a list with another list (or an arbitrary iterable), you can use the slice-assignment syntax:
A = B = [1,2,3]
A[:] = [4,5,6,7]
print(A) # [4,5,6,7]
print(A is B) # True
Slice assignment is implemented on most of the mutable built-in types. The above assignment is essentially the same the following:
A.__setitem__(slice(None, None, None), [4,5,6,7])
So the same magic function (__setitem__) is called when a regular item assignment happens, only that the item index is now a slice object, which represents the item range to be overwritten. Based on this example you can even support slice assignment in your own types.
The following is an except from a tutorial.
The default value is evaluated only once. This makes a difference when the default is a mutable object such as a list, dictionary, or instances of most classes. For example, the following function accumulates the arguments passed to it on subsequent calls:
def f(a, L=[]):
L.append(a)
return L
print f(1)
print f(2)
print f(3)
This will print
[1]
[1, 2]
[1, 2, 3]
However, when I try this with a scalar variable:
>>> def acu(n, a = 0):
"Test if local variables in functions have static duration"
a = a + n
return a
>>> acu (5)
5
>>> acu (5)
5
So why is this difference between the lifetimes of L and a?
There is no difference. In the second part you are rebinding the local name a, not mutating the object it points to.
Python Tutorial -
Important warning: The default value is evaluated only once
...
If you don’t want the default to be
shared between subsequent calls, you
can write the function like this
instead:
def f(a, L=None):
if L is None:
L = []
L.append(a)
return L
I still was expecting:
print f(1) # [1]
print f(2) # [1,2]
print f(3) # [1,2,3]
I reason:
The default value (L=None) was executed for f(1) which helped L point to a new empty list in the fn body. However on successive calls, L=None was not executed; so L still points to the list which already has 1 in it now, and subsequent calls are simply appending more elements to it thereby sharing L.
Where am I thinking incorrectly?
UPDATE
def f(a, L=[]):
L.append(a)
return L
Does L here point to an empty list created in heap or stack?
L is the name of an argument, but it is also a local variable. Rebinding it rebinds the local variable, but does not change the default argument.
UPDATE EDIT:
Python doesn't have "heap" and "stack" in the same manner as C; all it has are objects, and references to those objects. Your function call returns a reference to the same list that was created as the default value for the L argument of the f function, and any operation that mutates it will mutate the value of the default argument.
The default value is evaluated only once
means that if you do def foo(a, b=len([1,2,3])) b will be set to 3 and it will not make any function call to len if you call foo.
The = operator assigns an object to a name. It doesn't change the previous object.
If you run the functions below, it will help you to see how it works.
def f(a, L=[]):
print("id default: ", id(L))
L.append(a)
print("id used: ", id(L)
return L
Notice 1 object address, we are using only the default list object and we are changing it.
def f(a, L=[]):
print("id default: ", id(L))
if L == []:
L = []
L.append(a)
print("id used: ", id(L))
return L
Notice 2 different object addresses, when you assign L=[] again in the function, you are using a different list object and not the default list object, and that is why the default list object doesn't change.
def f(a, L=None):
print("id default", id(L))
if L is None:
L = []
L.append(a)
print("id used: ", id(L))
return L
This function is basically the same as the one above it, same idea, but it uses a None object instead of an empty list object.