Gaurav Khandelwal
Gaurav Khandelwal

Reputation: 109

Why can't I change the default value of a function after it was defined?

i = 5

def f(arg=i):
    print(arg)

i = 6
f()

I am learning Python from the official documentation. There I find the above piece of code which I am unable to understand as to why 5 is printed instead of 6. I am relatively new to Python. Can somebody help me understand the concept?

Upvotes: 7

Views: 2229

Answers (6)

Mark Tolonen
Mark Tolonen

Reputation: 178409

What others have said is true...the default is evaluated at the time of function creation, but it is not that it takes the "value of i" at the time of creation. The default is assigned the object referred to by "i" at the time of creation. This is an important point, because if that object is mutable, the default can be changed!

Here's what happens:

import inspect

i = 5   # name "i" refers to an immutable Python integer object of value 5.
print(f'i = {i} (id={id(i)})')  # Note value and ID

# Create function "f" with a parameter whose default is the object
# referred to by name "i" *at this point*.
def f(arg=i):
    print(f'arg = {arg} (id={id(arg)})')

# Use the inspect module to extract the defaults from the function.
# Note the value and ID
defaults = dict(inspect.getmembers(f))['__defaults__']
print(f'defaults = {defaults} (id={id(defaults[0])})')

# name "i" now refers to a different immutable Python integer object of value 6.
i = 6
print(f'i = {i} (id={id(i)})') # Note value and ID (changed!)
f()  # default for function still referes to object 5.
f(i) # override the default with object currently referred to by name "i"

Output:

i = 5 (id=2731452426672)               # Original object
defaults = (5,) (id=2731452426672)     # default refers to same object
i = 6 (id=2731452426704)               # Different object
arg = 5 (id=2731452426672)             # f() default is the original object
arg = 6 (id=2731452426704)             # f(i) parameter is different object

Now see the results of a mutable default:

import inspect

i = [5]  # name "i" refers to an mutable Python list containing immutable integer object 5
print(f'i = {i} (id={id(i)})')  # Note value and ID

# Create function "f" with a parameter whose default is the object
# referred to by name "i" *at this point*.
def f(arg=i):
    print(f'arg = {arg} (id={id(arg)})')

# Use the inspect module to extract the defaults from the function.
# Note the value and ID
defaults = dict(inspect.getmembers(f))['__defaults__']
print(f'defaults = {defaults} (id={id(defaults[0])})')

# name "i" now refers to a different immutable Python integer object of value 6.
i[0] = 6  # MUTATE the content of the object "i" refers to.
print(f'i = {i} (id={id(i)})') # Note value and ID (UNCHANGED!)
f()  # default for function still refers to original list object, but content changed!
i = [7]  # Create a different list object
print(f'i = {i} (id={id(i)})') # Note value and ID (changed)
f(i)     # override the default currently refered to by name "i"

Output:

i = [5] (id=2206901216704)            # Original object
defaults = ([5],) (id=2206901216704)  # default refers to original object
i = [6] (id=2206901216704)            # Still original object, but content changed!
arg = [6] (id=2206901216704)          # f() default refers to orginal object, but content changed!
i = [7] (id=2206901199296)            # Create a new list object
arg = [7] (id=2206901199296)          # f(i) parameter refers to new passed object.

This can have strange side effects if not understood well:

>>> def f(a,b=[]):    # mutable default
...   b.append(a)
...   return b
...
>>> x = f(1)
>>> x
[1]
>>> y = f(2)    # some would think this would return [2]
>>> y
[1, 2]
>>> x           # x changed from [1] to [1,2] as well!
[1, 2]

Above, b refers to the original default list object. Appending to it mutates the default list. Returning it makes x refer to the same object. The default list now contains [1] so appending in the 2nd call make it [1,2]. y refers to the same default object as x so both names refer see the same object content.

To fix, make the default immutable and create a new list when the default is seen:

>>> def f(a,b=None):
...   if b is None:
...     b = []
...   b.append(a)
...   return b
...
>>> f(1)
[1]
>>> f(2)
[2]

Upvotes: 1

Andrew
Andrew

Reputation: 1594

This is the difference between something being handled by reference vs by value. When you defined the function f you told it to set the argument's default value to i this is done by value, not by reference, so it took whatever the value of i was at that time and set the default for the function to that. Changing the value of i after that point does not change the value of arg. If you want it to work that way you could do this:

i = 5
def f(arg = None):
    if (arg = None)
        arg = i
    print(arg)

i = 6
f()

This lets you pass a value for arg into the function as normal, but if you don't (or you explicitly pass None) it updates arg to the current value of i if arg is still None (Python's version of NULL if you're familiar with other languages)


Something similar can be done using the or operator, arg = arg or i,but that will check if arg is falsy, and when using integers like you are in your example, 0 will be caught by the check.

Upvotes: 1

Jason Granzow
Jason Granzow

Reputation: 21

This is because you are assigning the value when the function is created. arg at the time of creation will be defaulted to what i is in that moment. Since at the time of the function being created the value of i is 5 then that's what the default value of that argument becomes. After the initial creation of the function i in the function argument is no longer linked to i in the body.

Upvotes: 0

altermetax
altermetax

Reputation: 706

i = 5
def f(arg=i)
    print(arg)

The i is evaluated at the time of definition, so the code above has the same meaning as the code below:

def f(arg=5)
    print(arg)

This means that, when the function is called without arguments, arg will have the value 5, no matter what the value of i is now.

In order to get what you want, just do the following:

def f(arg)
    print(arg)

i = 6
f(i)

Upvotes: 5

Unguest
Unguest

Reputation: 21

Because the function takes its default value on the first declaration of 'i'.

Change to i=6 on the first line if you want you code to print 6.

Hope I helped !

Upvotes: 2

Ted Brownlow
Ted Brownlow

Reputation: 1116

def f(arg=i) says "make me a function f where the default value for arg is whatever i is right now". At the time of defining the function, i=5.

Upvotes: 10

Related Questions