How to remove an object from a list during an iteration over the list itself

Asked

Viewed 339 times

3

I need to remove an item from a list during an iteration over the list itself.

The reason to remove during the loop is that if this item remains may end up triggering other triggers within the loop which would lead to a false result since the tests I do on the object within the list should be sequential.

What is the most appropriate (efficient/idiomatic) way to remove an object from a list when iterating over it?

lista=[] # Lista de obejos ativos.
class teste():
    ent = 0
    sai = 0

def alt(i):
    t = teste()
    t.ent = i
    t.sai = 2*i
    lista.append(t)
    print(t.ent,t.sai)

def objetos_ativos(x):
    print('O objeto ({} , {}) está ativo: '.format(x.ent, x.sai))

for i in range(5):
    alt(i)
print('')
#  Se usar ' lista.copy() ' para fazer a iteração sobre a  lista,  
#    x irá acionar a 'def objetos_ativos' e printar (20 , 4) como objeto ativo.
for x in (lista.copy()):
    # Trecho com problema
    if x.sai==4: # Se objeto 'x' falhar nesse teste ele é desativado imediatamente.
        lista.remove(x)
    #
    x.ent = x.ent * 10
    # Segundo teste que se o objeto foi desativado não deve participar.
    if x.ent >= 1:
        objetos_ativos(x)

for x in lista:
    print(x.ent, x.sai)

The entrance is:

0 0
1 2
2 4
3 6
4 8

And I waited for the exit as:

0 0
10 2
30 6
40 8

But instead it returns:

0 0
10 2
3 6
40 8
  • 1

    have to use the word continue to move to the next iteration or to exit the loop if there is no more, so you will not try to make "x.ent = x.ent * 10" an error, I suppose.

  • Thanks for the tip @André this along with the hkotsubo solution was exactly what I was looking for.

2 answers

4


To understand what happens, let’s modify your loop:

for i, x in enumerate(lista):
    print(f'Pegando elemento {i} = {x.ent}, {x.sai}')
    if x.sai == 4:
        lista.remove(x)
    x.ent = x.ent * 10

I use enumerate to iterate through the list to get the index and its element. The output is:

Pegando elemento 0 = 0, 0
Pegando elemento 1 = 1, 2
Pegando elemento 2 = 2, 4
Pegando elemento 3 = 4, 8

To understand the above output: the list originally has 5 elements:

[ (0, 0), (1, 2), (2, 4), (3, 6), (4, 8) ]

Up to the item in index 2 (ie, (2, 4)), everything happens normally. But then you remove this element from the list, then the list becomes:

[ (0, 0), (1, 2), (3, 6), (4, 8) ]

And in the next iteration of for, he tries to pick up index 3, which now corresponds to the element (4, 8) (i.e., the (3, 6) was "jumped").

This is the problem of removing items from a list while iterating on it.


An alternative is you build another list containing only the elements you want:

class teste:
    def __init__(self, ent=0, sai=0):
        self.ent = ent
        self.sai = sai

# cria a lista...

outra_lista = [ teste(x.ent * 10, x.sai) for x in lista if x.sai != 4 ]
for x in outra_lista:
    print(x.ent, x.sai)

I also added a constructor to the class teste, so I can create it already passing the values of ent and sai. And to build the new list, I used the syntax of comprehensilist on, much more succinct and pythonic (but nothing stops you from making a loop normal and populate the other list).

The above code does not modify the original list, but if you want to change it directly, an alternative is to:

lista[:] = ( teste(x.ent * 10, x.sai) for x in lista if x.sai != 4 )

The "trick" is the [:], which creates a Slice (a "chunk" of the list, which in this case is the entire list). Thus, the list is replaced by the new values.


The documentation still cites another alternative, which is to iterate on a copy of the list:

for x in lista.copy():
    if x.sai == 4:
        lista.remove(x)
    x.ent = x.ent * 10

Thus, removing elements from the list does not interfere with the iteration, since a copy is made.


About the problem of calling the function objetos_ativos, simply do not call if it has been removed (or, in this case, only call if the element is not removed). Then a block else would already solve:

for x in lista.copy():
    if x.sai == 4:
        lista.remove(x)
    else:
        x.ent = x.ent * 10
        if x.ent >= 1:
            objetos_ativos(x)
  • Hello hkotsubo, thank you very much for the explanation, very didactic and instructive. I could understand why of the error and how to correct for this case. But as I said, this code is an example. I don’t know if I was clear enough in the explanation, but in the real case the loop continues and other tests are done on the object 'x' which can cause 'x' to call other functions inside loop which shouldn’t happen because 'x' is no longer active after failing the test 'x.sai == 4'.

  • 1

    @Thalesalmeida In that case I suggest you edit the question and put a closer example of the real case, so I can update the answer as well

  • @Thalesalmeida I updated the answer (I hope I understood the problem)

  • 1

    It worked, thank you very much. I tested now also the solution with the "continue" that André suggested + the copy of the list you suggested and it worked as well. Thank you very much for the explanations.

1

Hello, Thales.

It seems the error consists of removing the object from the list that is serving as parameter for the iteration.

A solution would be to store this parameter in a temporary variable and, after its completion, use this parameter to remove the object from the list, as follows:

lista=[]
class teste():
    ent = 0
    sai = 0

def alt(i):
    t = teste()
    t.ent = i
    t.sai = 2*i
    lista.append(t)
    print(t.ent,t.sai)


for i in range(5):
    alt(i)
print('')

for x in lista:
    x.ent = x.ent * 10
    if x.sai == 4:
        temp = x
lista.remove(temp)

for x in lista:
    print(x.ent, x.sai)

I hope I helped. Hugs.

  • Hello John, thank you for the answer. I edited my question to better explain my problem. In the actual case I need that after x is removed from the list it no longer continues to interact with the code of the loop and call other functions.

Browser other questions tagged

You are not signed in. Login or sign up in order to post.