[Author Prev][Author Next][Thread Prev][Thread Next][Author Index][Thread Index]

Re: [pygame] more list problems



Although I think Noah provided the "correct" solution, which is to create a new list by filtering the input list, I figured you also might want to know why your solution below doesn't work as you intend.

Iterating over changing mutable sequences is tricky because as the list changes, the indexes of elements in the list can also change. Imagine the following list of rects:

rect = [Rect(10,10,5,5), Rect(0,0,10,10), Rect(7,7,3,3)]

Now imagine a "click" at position (8,8)

range(0, len(rect)) returns [0, 1, 2] (Note you can omit the first argument, but default range() starts at index 0, thus range(len (rect)) is equivalent)

The first time though the loop, i=0. The mouse position is not inside rect[0], so we move on. Now i=1, and the mouse is inside rect[1], so we now execute 'del rect [1]', thus changing the list to:

[Rect(10,10,5,5), Rect(7,7,3,3)]

The next time through the loop i=2, but now the rect list does not have an index 2! So it blows up with an IndexError. Note this is not the only possible outcome. Had my list been longer I would have wound up skipping the original element at index 2 altogether, meaning it would never be checked. Those are the kinds of bugs that can make you tear your hair out 8^)

So assuming we needed to mutate the list in this way (and not filter it into a new one as Noah suggested), what could we do?

Let me start by saying that anytime I find myself writing a for loop over a contrived range(), I pause and think for a second, because in Python that is almost never the best way to iterate something. It's a bit of a red flag. That's not to say I never do it, it's just not typical, even though it's often the only way to do loops in other languages.

To fix the above, I would suggest iterating over a copy of the original list instead of the range(). That way you can mutate the original list without upsetting the loop. you can create a (shallow) copy of a list by passing it to the built-in list():

for r in list(rect): # iterate over a copy of rect list
	if r.collidepoint(mousexTEMP, mouseyTEMP):
		rect.remove(r)

Note I omitted the numRect calculation. Can't you just use len(rect) at the end of the loop?

Now having "solved" the problem of mutating the rect list in this way, I have to say that Noahs solution is more elegant, thus superior to the above. It doesn't mutate the list in place at all, thus skirting the problem entirely, plus it's much shorter.

The problem you encountered is a very common python coding pitfall. The best way to avoid it is to think twice before mutating a sequence while iterating it.

hth,

-Casey

On Sep 20, 2007, at 5:18 PM, Eric Hunter wrote:

so I have been able to write pygame.Rect rectangles into rect-type lists. but now I'm trying to delete unwanted rectangles that are printed to the screen.

when I draw one at a time, I can only delete the last drawn rectangle before deleting the previous ones. I want to be able to delete any rectangle at any time without problems. I've come close but have run into a wall.

below is the "deleting rect" section. if anyone needs more then what's provided i can provide it upon request.

thanks in advance.

[code]
if event.button == 3:
mousexTEMP, mouseyTEMP = pygame.mouse.get_pos()
                                for i in range(0, len(rect)):
if rect[i].collidepoint (mousexTEMP, mouseyTEMP):
                                            del rect[i]
                                                numRect -= 1
[/code]