Search code examples
pythonlistloopsinstance

Why is this method in my function being called until the list in which it is contained in goes out of range?


I have an object which is a list of list of list. It is used to hold a list of object in 2d matrix like format. Kinda like this:

[[ [], [], [], ], [ [], [], [], ]]

Let's call this World. World is a class that is instantiated on startup of program to create a blank list like above. The reference to the instanciated object is saved in a separate method called singleton.

An object can be added to the world using add method defined in world.

def add(self, entity, posx, posy):
    print(posx, posy)
    self.world[posx][posy].append(entity)

entity is the object to be added. And posx, posy determines on which list the object goes in.

The objects are supposed to have a method that is called repeatedly after a set interval.

To iterate over the World I used this code:

def apply_entity_logic_all():
    for i in range(len(singleton.World.world)):
        for j in range(len(singleton.World.world[i])):
            for id, entity in enumerate(singleton.World.world[i][j]):
                entity.act(i, j, id)

In entity.act all I want is to move the object in question to another list in World. Sort of like moving it to another cell in the grid. To do that I tried to copy the object over to the new cell and delete the previous one. Like this:

def act(self, i, j, id):
    new_object = Plankton(
        posx=i+1, posy=j)
        del singleton.World.world[i][j][id]
        singleton.World.add(new_object, i+1, j)

Plankton is a class inherited by Entity. In this case it does not affect the problematic behaviour.

The problem here is new_object is being created and added to the world until it goes out of bound. It is supposed to be called only once. I can say that because in another similar function it works. Only entity.act() line is different and it deletes the said entity instead.

I presume the problem is with my act() function. But I cannot identify what is the problem. Thanks in advance.

I tried using python's deepcopy() instead of creating it new. It didn't change anything. When I loaded it up in pdb it seems that when act() is triggered for reasons I cannot explain the creating of new object lines gets called until the space in list runs out.

Edit: After further testing I determined that only this line singleton.World.add(new_object, i+1, j) is being called multiple times. Others are called only once. There is no loop before there. I have no idea why is that happening.


Solution

  • The issue arises from how you are modifying the list while iterating over it. When you use del singleton.World.world[i][j][id] to delete an element from the list, you're changing the list that you're currently iterating over, which can lead to unexpected behaviors.

    The for loop in Python does not dynamically update the list that it is iterating over. So, when you delete an object in the list that you're iterating over, you effectively shift all subsequent items down one index. When you then move to the next index via for id, entity in enumerate(singleton.World.world[i][j]), you're skipping over an entity because everything has moved down one index.

    Moreover, you're creating a new entity in the act() method and then immediately add it to the same list you're currently iterating over. That could create a kind of loop where you're iterating over new entities that you're adding while the iteration is still ongoing.

    Here's a safer way to do it:

    1. First, iterate through your 2D list and apply act(), but store the entities to be removed and added in separate lists. Do not alter the world within the act() function.
    2. After the iteration, apply the changes to the World.

    This could look something like this:

    def apply_entity_logic_all():
        to_be_removed = []
        to_be_added = []
    
        for i in range(len(singleton.World.world)):
            for j in range(len(singleton.World.world[i])):
                for id, entity in enumerate(singleton.World.world[i][j]):
                    removal, addition = entity.act(i, j, id)
                    to_be_removed.append((i, j, id)) if removal else None
                    to_be_added.append((addition, i+1, j)) if addition else None
    
        for (i, j, id) in to_be_removed:
            del singleton.World.world[i][j][id]
    
        for (entity, x, y) in to_be_added:
            singleton.World.add(entity, x, y)
    
    def act(self, i, j, id):
        new_object = Plankton(posx=i+1, posy=j)
        return (True, new_object)  # return a tuple indicating that current object should be removed and new one should be added
    

    Please note that you might need to reverse the to_be_removed list to ensure you delete entities starting from the end of the list. This is because deletion of elements at a lower index would cause shifting of elements at higher indices, which may lead to an IndexError.

    for (i, j, id) in reversed(to_be_removed):
        del singleton.World.world[i][j][id]
    

    These changes ensure that you're not modifying the list while you're iterating over it, and should prevent the issue you're seeing.