Avoiding Further Gotchas: Mutable Default Arguments & Copying Objects
00:00 And in this lesson, let’s look at another pitfall you should be aware of when using mutable data types.
00:08
Let’s define a function called append_to()
. Its first parameter is item
, and the second parameter is target
. But target
has a default value, which in this case is the empty list.
00:22 Now, you’ll see later in this lesson that this is not the ideal way of solving this problem, so we’ll look at the proper way later on. But for the time being, the default value is the empty list.
00:34
You can now call the append()
method. So target.append()
and pass item
to it, and then return target
.
00:42
This is a toy function, a demonstration function, so it’s only there to illustrate a point. The way this function is intended to be used is that you can either append an item to an existing list if you pass it as its second argument, or, if you only use the function with one argument that’s assigned to item
, then you would like the function to create a new list and use an empty list instead of an existing one.
01:11
So you can start by calling the function append_to()
, and let’s use the default option.
01:18
And there you go, it creates a list which includes 1
, which was the argument. You passed the function, and this makes sense. It’s what you expect it to do.
01:26
But if you call the function again, another time, relying on the default value, so including an argument for item
but not for target
, the output is now list [1, 2], which is not what you are expecting.
01:42 The reason this happens is that Python defines the default argument when it passes the function for the first time. It does not overwrite the default argument every time you call the function.
01:55 And since a list is mutable, when you define the function, you’re creating a list which is going to be used as a default argument. This is always the same list, and therefore, each time you call the function, if you are relying on the default argument, Python will always call the same object, the same list, and not a new empty list every time.
02:17 Therefore, you should not use mutable objects as default arguments when you define functions.
02:25
So let’s look how you would solve this problem the proper way. Let’s redefine append_to()
. It still has item
as its first parameter and target
as it’s second. target
still has a default value, but instead of using a list which is a mutable type, you want to use an immutable type.
02:43
And the ideal one is None
, Python’s object that shows that there is nothing or an object that refers to nothing. Now, you might be thinking, yeah, but I want a list, not None
.
02:53
So what you’d be able to do is to start off your function by saying, if target
is None
, that means if the function was called
03:00
with no second argument, and therefore target
is None
, what can you do? Now you can set target
to be equal to an empty list.
03:10
This is different because this empty list is being created when the function is called, not when the function is defined. Every time you call the function, if target is None
, at that point, you’re creating an empty list, and then you can carry on as normal.
03:27
So now you can say target.append(item)
and finally, you can return target
to the list. So in this case, if you call append_to()
with an existing list, that’s the list that’s going to be used.
03:42
If you do not pass a list, target
will be None
that’s a default value, and the function will create an empty list. You can confirm by, once again, let’s call append_to()
with 1
, the integer 1
, and this is what we expect.
03:59
The problem earlier was when we called the function again, in this case, if you call the function with one argument, you start with an empty list. And in this case, the value 2
is added to that list.
04:12 This now works as intended.
04:17 And here’s something else you need to be aware of when dealing with mutable data types. Let’s assume you have a matrix, which is a list of lists. There’s the outer list, which contains three items, three lists, each one of them has three numbers.
04:32
You can create a copy, let’s call it copied_matrix
of the list. And since it’s a list, you can call the copy()
method to create a copy of the matrix.
04:44
So matrix
is the list we had before, and copied_matrix
is a copy of the list. You can confirm that they’re not the same object.
04:53
So the id
of matrix
is a number that ends with 6848, whereas the id
of copied_matrix
05:03
is a different identity showing that these are different objects. However, let’s try to find the id
of the first element in matrix
.
05:15 So this is the list with 1, 2, 3. Within it, it’s a list with an identity with a number that ends in 4800.
05:25
How about the identity of the first item in copied_matrix
?
05:31
And you notice that it’s the same identity. Therefore, even though matrix
and copied_matrix
are different objects, they’re different lists, their contents are the same.
05:44
This is what’s called a shallow copy, which means that even though copied_matrix
is a new object, the items within it are a reference to the same objects that matrix
has.
05:56
And this can be confusing if you’re not expecting it. If this is not the behavior you want, you can use the copy
module from the standard library and you can create what’s called a deep copy of the matrix using copy.deepcopy()
06:13
and passing matrix
to it.
06:17
And in this case, deep_copy
is not just a new object, but its contents are also new objects. So the id
of deepcopy[0]
06:31
is no longer the same as the number that ends with 3152, which is not the same object that was in the first item of matrix
, which had the identity ending in 4800.
06:45 So let’s finish off this lesson by summarizing some of the common mutability-related gotchas you need to be aware of. First one is aliasing variables. You can have two variables that refer to the same object, and if that same object is mutable or immutable, the behavior will change because if you try to, for example, increment an immutable type Python will create a new object.
07:11
But that’s not the case if you try to increment using +=
, for example, mutable objects such as a list. You also have to be careful with arguments you pass into a function, your function, when you define the function, you should not mutate the arguments that you pass.
07:27 This will give you side effects that are unlikely to be what you want your program to do. So avoid mutating the arguments you pass into a function within the function itself.
07:39 And as we’re dealing with functions, when you use default values and functions, do not use mutable types because this can give you unexpected results when you call the function repeatedly using the default value.
07:54 And you should also be familiar with the difference between shallow and deep copies. Since Python’s lists, for example, are mutable, it’s often common to make a copy of a list if you want to perform operations without changing the original.
08:09 However, by default, that creates a shallow copy, which means that even though you’re creating a new list, its contents will be the same contents as in the original list.
08:20 If you want the contents of a list to also be copies, then you need to create a deep copy.
08:26 So even though mutable data types give you more flexibility than immutable types, there are some pitfalls you need to be aware of, as well as design considerations on whether you want immutable or an immutable data type for your application.
Become a Member to join the conversation.