Python Introducing Lists Using Lists Mutability

Michael Miller
Michael Miller
6,329 Points

.remove() and .copy()

I get that you can't properly use .remove() in a for loop, but why? What's the logic that makes the result in the example leave index 1 & 3 (IIRC)? Also, if .copy() doesn't actually save a new instance of the variable and you're still editing the original variable.... How does that function? How does .copy() make it possible to use .remove() in a for loop? I get that it does, but why and how?

4 Answers

andren
andren
28,386 Points

I get that you can't properly use .remove() in a for loop, but why?

Let's say you had code like this:

numbers = [0, 1, 2, 3, 4, 5]
for number in numbers:
    numbers.remove(number)

When the for loop starts Python pulls out the first item of the numbers collection and assigns it to number as I'm sure you already understand, but how does Python do that? It simply uses the index the same way as you would if you were to do so manually. So behind the scenes the first iteration of the loop essentially looks like this:

numbers = [0, 1, 2, 3, 4, 5]
number = numbers[0] # Index 0 contains 0
numbers.remove(number) # Remove 0 from list

Well what happens the next iteration? Python pulls out the second item by simply increasing the index it used in the previous iteration. So after two iterations the loop would essentially have executed code similar to this:

numbers = [0, 1, 2, 3, 4, 5]
number = numbers[0] # Index 0 contains 0
numbers.remove(number)  # Remove 0 from list
number = numbers[1] # Index 1 contains 2
numbers.remove(number) # Remove 2 from list

Now looking at the comments you will notice that we have suddenly skipped past the number 1, why? Well because when 0 was removed the indexes all shift back by one. The first item is now 1, the second 2, the third 3 and so on, since 0 is no longer an item in the list.

But Python's for loop has not taken this change into account, it assumes that the indexes are unchanged which is why it ends up skipping the number 1. And that behavior continues in the subsequent iterations and results in the loop essentially skipping every other item in the list.

Also, if .copy() doesn't actually save a new instance of the variable and you're still editing the original variable.... How does that function?

I think you must have misunderstood something about how copy works. The method does exactly what the name implies, it creates a copy of the variable, and this copy is entirely independent of the original. Modifying the copy will not affect the original in any way.

Michael Miller
Michael Miller
6,329 Points

That first part makes perfect sense! Thank you so much!

The second part... I feel as though I'm on the brink of understanding it.

So, by using .copy() you create an actual copy that is used for the purposes of the original within the for loop without actually editing the original, that way nothing is changed outside the for loop, and each time you iterate .remove() it's on a completely different copy, therefore bypassing the issue of the index shifting as the removed index increments...?

So... it references the index of the copy and removes that item from the original list...?

I'm still a little confused.

andren
andren
28,386 Points

Yep that's pretty much it. When you want to loop through a list while removing things from it (not that uncommon actually) there are basically two common approaches. You can create a copy of the list and store it in a variable and then remove items from that copy during the loop, or you can use a copy as the source of the loop and then remove items from the real list.

The code shown off in the end of the video is an example of the latter:

inventory = ["shield", "apple", "sword", "bow", "boomerang"]
for item in inventory.copy(): # A copy of inventory is made which is what the loop actually goes through
    inventory.remove(item) # items are removed from the real list

Since the loop is iterating though the copy of inventory (which is not affected by changes to inventory) the loop is not disturbed by the remove operation. One easy way to think of it is that the copy method essentially just creates a new list that is filled with the same items as the first list. So the above code can be seen as equivalent to typing this:

inventory = ["shield", "apple", "sword", "bow", "boomerang"]
for item in ["shield", "apple", "sword", "bow", "boomerang"]: 
    inventory.remove(item)

The loop is iterating through a list containing the same items as inventory, but it is technically speaking not the same list being iterated through.

fady mohamed
fady mohamed
5,063 Points

Hey Andren,

Thanks heaps, your explanations helped me as well. I just have a question with this part

You can create a copy of the list and store it in a variable and then remove items from that copy during the loop

I understand it would work if the copy is made within the loop as the list is remade every iteration. however if the copy is made prior then wouldnt the items be removed from the copy you've made as well? thereby producing the same problems?

andren
andren
28,386 Points

In the scenario where you are removing items from the copy you would have the loop iterate though the real list, not the copy. To illustrate here are code examples of the two approaches I mentioned.

Storing the copy in a variable:

inventory = ["shield", "apple", "sword", "bow", "boomerang"]
inventory_copy = inventory.copy() # Create a copy of the list
for item in inventory # Iterate through real list
    inventory_copy.remove(item) # items are removed from the copy

In the above scenario you store the copy in a variable and use that within the loop, but the source of the loop is the real list. Which is not affected by anything done to its copy, just like the copy is not affected by any changes done to the real list. This approach is mainly used in scenarios where it is useful to have access to both the original list and a version of the list that has been manipulated by the code within the loop.

Using the copy as a source:

inventory = ["shield", "apple", "sword", "bow", "boomerang"]
for item in inventory.copy(): #  Iterate through a copy of the list
    inventory.remove(item) # items are removed from the real list

Since removing items from the real list does not affect the copy the above loop will work just fine. In scenarios where you don't need access to the original list after the loop has run this approach is more efficient, since you don't end up creating an extra variable. The copy that you create for the loop is disposed of automatically as Michael mentioned in his answer.

Michael Miller
Michael Miller
6,329 Points

fady, not quite. It makes a copy of the list at the moment of creating the loop, then references that copy as it removes items from the original. The copy isn't altered during the loop at all and then... I believe it is discarded afterwards as it isn't assigned a variable. That's my understanding, at least.

Margot Waterman
Margot Waterman
2,419 Points

thank you andren! and michael for asking =)

Daniel Murray
Daniel Murray
3,294 Points

Andren thank you for this. Very helpful, I understand it perfectly now.