When you define functions in Python, you can specify default arguments to them so that you don’t have to pass them in every time the function is called, and it simplifies your code.
When you define a function, there are often side effects if you use a mutable type as the default argument. Take a look at the following code.
def foo(li=[]) :
li.append(1)
print(li)
foo()
foo()
foo()
Copy the code
You might expect something like this:
[1]
[1]
[1]
Copy the code
But in reality, the result is:
[1] [1, 1]Copy the code
Based on the results, it appears that for each function call, the li list records the result of the previous call without using the default argument []. But when we pass an empty list [] every time we call the function, there are no side effects and we get exactly what we want.
def foo(li=[]) :
li.append(1)
print(li)
foo([])
foo([])
foo([])
Copy the code
[1]
[1]
[1]
Copy the code
Why did this happen? The problem is due to the nature of Python functions. Since functions are objects, and objects can have their own attributes, functions also have their own attributes.
When foo is created, its default arguments are already stored in its __defaults__ property. The function is created only once, and each subsequent execution of foo() only calls the function, not recreating it. So the default arguments to a function are evaluated only once, and remain the same object no matter how many times it is called afterwards.
We can see this by using the id() function to print out the memory address of the default argument.
def foo(li=[]) :
li.append(1)
print(li, id(li))
foo()
foo()
foo()
Copy the code
[1] 48904632
[1, 1] 48904632
[1, 1, 1] 48904632
Copy the code
As you can see, after three calls to the function, the internal printed li address is the same, so they are actually the same object.
Knowing the cause of the problem, what about solving it? We can use None as the default argument to a function. When calling foo, we can use the default value inside the function body by checking whether li is None. Thus, when foo() is called and no arguments are passed, give li a default value.
def foo(li=None) :
if li is None:
li = []
li.append(1)
print(li)
foo()
foo()
foo()
Copy the code
[1]
[1]
[1]
Copy the code
This way, you don’t have to worry about the side effects of using mutable types as default arguments, and you don’t have to pass an argument into foo every time you call it.
Therefore, when defining default parameters for functions, we should try to use immutable types to avoid unintended side effects. Unless, of course, you explicitly know that you need mutable type features for some purpose.