Python Forum

Full Version: removing items from a list or group within a for loop.
You're currently viewing a stripped down version of our content. View the full version with proper formatting.
Hello, hello.
I'm learning Python with a book(Python Crash Course), and at the part to code a game called 'Alien invasion'. To be more specific, currently I'm following to code bullets to be fired from a ship.

I have encountered this below.
# Get rid of bullets that have disappeared.
for bullet in self.bullets.copy():
    if bullet.rect.bottom <= 0:
        self.bullets.remove(bullet)
and, the author wrote,
"When you use a for loop with a list (or a group in Pygame), Python expects that the list will stay the same length as long as the loop is running. Because we can't remove items from a list or group within a for loop, we have to loop over a copy of the group. We use the copy() method to set up the for loop (line2), which enables us to modify bullets inside the loop."

I have 6 questions.

1 - why does Python like the length of a list to be the same within a for loop?
2 - is it common to keep the same length of list within for loop in programming languages?
3 - How does it possibly modify the original list by making a copy of it?????????!!!!!!!!!!!
4 - what about a while loop?
5 - what other solution is out there to modify a list within a for loop?
6 - is this sentence "it is not safe to modify the list during an iterative looping" the same as the sentence "Python expects that the list will stay the same length as long as the loop is running"?

Thank you in advance.
1: Say you are looping through a list. You get to the second item in the list, and decide to remove it. When the loop iterates again, it goes to the third item in the list. But the third item is now what the fourth item was before you removed the second item, and you just skipped over what was the third item because it is now the second item. If you find this confusing, so does Python.

2. I don't know.

3. It loops over the copy of the list, but then when it does the removal, it does that to the original list.

4. You could do that, but I expect it would be more complicated than looping over a copy.

5. I'm not used to seeing people make copies to loop over. More often I see people building new lists:

new_list = []
for item in old_list:
    if is_valid(item):
        new_list.append(item)
old_list = new_list
6. I'm not sure what Python expects, but it not being safe to mess with what you are looping over is a great way to think about it.
(Nov-10-2019, 09:09 PM)ichabod801 Wrote: [ -> ]1: If you find this confusing, so does Python.

3. It loops over the copy of the list, but then when it does the removal, it does that to the original list.

5. I'm not used to seeing people make copies to loop over. More often I see people building new lists:

new_list = []
for item in old_list:
    if is_valid(item):
        new_list.append(item)
old_list = new_list
6. I'm not sure what Python expects, but it not being safe to mess with what you are looping over is a great way to think about it.
1: Okay, I got it. Thank you.

3: The point of removing was not to slow down the program because the bullets will keep adding up. Isn't it counter-intuitive then to make a copy? because a copy of un-deleted bullets will still reside? What am I missing?

5. I need some time to understand your codes... keeping look at 'em 'til I digest but thank you.

6. Okay, got it.
A while loop that starts at the END of the list and works backward will avoid the skipping problem and is considered "safer", kind of like bungie jumping is safer than cliff diving. Also, you can just remove items using that remove function but I think that would miss the point the instructor is trying to make.
lst = [1,2,3,4,5,6,7]
start = len(lst)-1
while start > -1 :
    if lst[start] == 4 :
        lst.remove(lst[start])
    start -= 1
print (lst)
lst = [1,2,3,4,5,6,7]
lst.remove(3)
print(lst)
Output:
[1, 2, 3, 5, 6, 7] [1, 2, 4, 5, 6, 7]
(Nov-11-2019, 12:37 AM)allusernametaken Wrote: [ -> ]3: The point of removing was not to slow down the program because the bullets will keep adding up. Isn't it counter-intuitive then to make a copy? because a copy of un-deleted bullets will still reside? What am I missing?

The copy will reside for a while. But as soon as the loop is done, all the references to the copy will be gone. As soon as garbage collection happens, that memory will be freed up. Also, the copy is going to be a copy of references to the objects, not full copies of all the objects in the list. So it takes up a lot less space than keeping those objects around would. It also avoids any processing time associated with those objects, such as checking them for collision events.
This is the most pythonic way to do such a task.
# Get rid of bullets that have disappeared.
self.bullets = [bullet for bullet in self.bullets if bullet.rect.bottom > 0]
Typing >>> help('for') into interactive interpreter will give pretty comprehensive description about for-loop behaviour. For example:

Quote:There is a subtlety when the sequence is being modified by the
loop (this can only occur for mutable sequences, e.g. lists). An
internal counter is used to keep track of which item is used next,
and this is incremented on each iteration. When this counter has
reached the length of the sequence the loop terminates. This means
that if the suite deletes the current (or a previous) item from the
sequence, the next item will be skipped (since it gets the index of
the current item which has already been treated). Likewise, if the
suite inserts an item in the sequence before the current item, the
current item will be treated again the next time through the loop.
This can lead to nasty bugs that can be avoided by making a
temporary copy using a slice of the whole sequence, e.g.,

     for x in a[:]:
         if x < 0: a.remove(x)
Hi everyone, I'm the author of PCC. This was an interesting issue to work through when I was first writing the book. I spent a fair bit of time thinking about how much to explain this bit of code.

One thing to realize is that bullets is an instance of pygame.sprite.Group; it is not a list. So you can't just make a new group with a comprehension. If you wanted to make a new group I believe you'd need to write something like this:

# Keep only the bullets that are still live.
remaining_bullets = Group()
for bullet in self.bullets:
    if bullet.rect.bottom >= 0:
        self.remaining_bullets.add(bullet)
self.bullets = remaining_bullets
That's more verbose, and less clear than the original code. You can use a comprehension to make a list of dead bullets, then loop through that list and remove those bullets from the group bullets:

dead_bullets = [bullet for bullet in self.bullets if bullet.rect.bottom <= 0]
for bullet in dead_bullets:
    self.bullets.remove(bullet)
I didn't find this approach particularly appealing when I first wrote the book, and I'm still not convinced this is better than what's in the text.

What do people think?
I don't see that verbose is necessarily worse, especially if you are looking for clarity. I don't see that it's less clear, either. If I am reading the Pygame docs correctly, you could do a list comprehension:

self.bullets = Group(*[bullet for bullet in self.bullets if bullet.rect.bottom <= 0])
I have read all the replies multiples times since yesterday, although I still understand only 50%. It's very cool to see the different approaches depends on different understanding. Most important lesson I got from this discussion is to read the documentation. Thank you all for the inspiration.