This question already has answers here:
How to remove items from a list while iterating?
(25 answers)
Closed 6 years ago.
I am trying to remove classes from a list based on their hp. I am making a large scale battle simulator for a D&D campaign. Its a simple program that makes two lists of classes and pits them against each other.
I am running into a problem when it comes to removing dead fighters. It works fine if one fighter dies in a round, but when multiple die, it goes wonky.
def check_human_deaths():
for i in range(len(goodguys)):
if goodguys[i].hp <= 0:
print('{} has died...'.format(goodguys[i].name))
goodguys.remove(goodguys[i])
Removing the dead fighter changes the length of the list, throwing an index error:
IndexError: list index out of range
I am not to sure how to proceed with removing the dead from the battlefield. Any tips are appreciated. Let me know if I am going about this in a fundamentally wrong way.
Two choices:
Modify a copy of the list and use the result of that as the new list:
>>> lst = [1,2,3,4,5]
>>> for i in lst[:]:
... if i % 2:
... result.append(i)
...
>>> lst = result
>>> lst
[1, 3, 5]
Modify the list in place, but do so in reverse to avoid messing up the indexes:
>>> lst = [1,2,3,4,5]
>>> for i in lst[::-1]:
... if not i % 2:
... lst.remove(i)
...
>>> lst
[1, 3, 5]
The problem is that when you remove from goodguys, the index is reduced by one. Ex:
1,2,3,4
Remove 2
1,3,4
Index of three has been decremented by one and the size has been decremented by one.
goodguys = [ guy for guy in goodguys if guy.hp > 0 ]
This will filter out any dead goodguys in your array.
You can use it in your function like this:
def check_human_deaths():
global goodguys
dedguys = [ guy for guy in goodguys if guy.hp <= 0 ]
for guy in dedguys:
print('{} has died...'.format(guy.name))
goodguys = [ guy for guy in goodguys if guy.hp > 0 ]
You are getting this error because you are iterating over the length of the list let's say l. Each time you call list.remove(), length of the list is decreased by 1, and your program gives index error because list[l] does not exists. Instead you can use:
def check_human_deaths():
for goodguy in list(goodguys):
if goodguy.hp <= 0:
print('{} has died...'.format(goodguy.name))
goodguys.remove(goodguy)
Note: list(goodguys) will create the copy of goodguys list preventing the weird behavior of for loop when object is removed from the list:
>>> t= [1,2,3]
>>> x= list(t)
>>> x.remove(1)
>>> x
[2, 3]
>>> t
[1, 2, 3]
Even after removing the value from x, this value will still be present in t and your for for loop will not behave weirdly
As others said, You can't change the list while iterating over it because weird things will happen. Instead You can save elements that You want to delete and delete them after the loop:
def check_human_deaths():
things_to_delete = []
for i in range(len(goodguys)):
if goodguys[i].hp <= 0:
print('{} has died...'.format(goodguys[i].name))
things_to_delete.append(goodguys[i])
goodguys = [x for x in goodguys if x not in things_to_delete]
You can do it using a generator function to allow you to print and update the list in a more efficient and correct manner doing a single pass over the list:
class Foo():
def __init__(self, hp, name):
self.hp = hp
self.name = name
def check_human_deaths(goodguys):
for guy in goodguys:
if guy.hp < 1:
print('{} has died...'.format(guy.name))
else:
yield guy
Demo:
In [29]: goodguys = [Foo(0,"foo"), Foo(1,"bar"), Foo(2,"foobar"), Foo(-1,"barf")]
In [30]: goodguys[:] = check_human_deaths(goodguys)
foo has died...
barf has died...
In [31]:
In [31]: print(goodguys)
[<__main__.Foo instance at 0x7fdab2c74dd0>, <__main__.Foo instance at 0x7fdab2c6e908>]
This answer has a very good explanation as to why you should never mutate a list you are iterating it.
Also if you are going to start at the end of the list and use remove, you can use reversed instead of creating another copy of the list:
for guy in reversed(goodguys):
if guy.hp <= 0:
print('{} has died...'.format(guy.name))
goodguys.remove(guy)
But that is not going to be as efficient as the first option.
Related
I'm currently developing a program in python and I just noticed that something was wrong with the foreach loop in the language, or maybe the list structure. I'll just give a generic example of my problem to simplify, since I get the same erroneous behavior on both my program and my generic example:
x = [1,2,2,2,2]
for i in x:
x.remove(i)
print x
Well, the problem here is simple, I though that this code was supposed to remove all elements from a list. Well, the problem is that after it's execution, I always get 2 remaining elements in the list.
What am I doing wrong? Thanks for all the help in advance.
Edit: I don't want to empty a list, this is just an example...
This is a well-documented behaviour in Python, that you aren't supposed to modify the list being iterated through. Try this instead:
for i in x[:]:
x.remove(i)
The [:] returns a "slice" of x, which happens to contain all its elements, and is thus effectively a copy of x.
When you delete an element, and the for-loop incs to the next index, you then skip an element.
Do it backwards. Or please state your real problem.
I think, broadly speaking, that when you write:
for x in lst:
# loop body goes here
under the hood, python is doing something like this:
i = 0
while i < len(lst):
x = lst[i]
# loop body goes here
i += 1
If you insert lst.remove(x) for the loop body, perhaps then you'll be able to see why you get the result you do?
Essentially, python uses a moving pointer to traverse the list. The pointer starts by pointing at the first element. Then you remove the first element, thus making the second element the new first element. Then the pointer move to the new second – previously third – element. And so on. (it might be clearer if you use [1,2,3,4,5] instead of [1,2,2,2,2] as your sample list)
Why don't you just use:
x = []
It's probably because you're changing the same array that you're iterating over.
Try Chris-Jester Young's answer if you want to clear the array your way.
I know this is an old post with an accepted answer but for those that may still come along...
A few previous answers have indicated it's a bad idea to change an iterable during iteration. But as a way to highlight what is happening...
>>> x=[1,2,3,4,5]
>>> for i in x:
... print i, x.index(i)
... x.remove(i)
... print x
...
1 0
[2, 3, 4, 5]
3 1
[2, 4, 5]
5 2
[2, 4]
Hopefully the visual helps clarify.
I agree with John Fouhy regarding the break condition. Traversing a copy of the list works for the remove() method, as Chris Jester-Young suggested. But if one needs to pop() specific items, then iterating in reverse works, as Erik mentioned, in which case the operation can be done in place. For example:
def r_enumerate(iterable):
"""enumerator for reverse iteration of an iterable"""
enum = enumerate(reversed(iterable))
last = len(iterable)-1
return ((last - i, x) for i,x in enum)
x = [1,2,3,4,5]
y = []
for i,v in r_enumerate(x):
if v != 3:
y.append(x.pop(i))
print 'i=%d, v=%d, x=%s, y=%s' %(i,v,x,y)
or with xrange:
x = [1,2,3,4,5]
y = []
for i in xrange(len(x)-1,-1,-1):
if x[i] != 3:
y.append(x.pop(i))
print 'i=%d, x=%s, y=%s' %(i,x,y)
If you need to filter stuff out of a list it may be a better idea to use list comprehension:
newlist = [x for x in oldlist if x%2]
for instance would filter all even numbers out of an integer list
The list stored in the memory of a computer. This deals with the pointer to a memory artifact. When you remove an element, in a by-element loop, you are then moving the pointer to the next available element in the memory address
You are modifying the memory and iterating thru the same.
The pointer to the element moves through the list to the next spot available.
So in the case of the Size being 5...enter code here
[**0**,1,2,3,4]
remove 0 ---> [1,**2**,3,4] pointer moves to second index.
remove 2 ---> [1,3,**4**] pointer moves to 3rd index.
remove 4 ---> [1,3]
I was just explaining this to my students when they used pop(1). Another very interesting side-effect error.
x=[1,**2**,3,4,5]
for i in x:
x.pop(1)
print(x,i)
[1, **3**, 4, 5] 1 at index 0 it removed the index 1 (2)
[1, **4**, 5] 3 at index 1 it removed the index 1 (3)
[1, 5] 5 at index 2 it removed the index 1 (4)
heh.
They were like why isnt this working... I mean... it did... exactly what you told it to do. Not a mind reader. :)
The objective of this function is to remove the first two occurrences of n in a list.
Below is a code I had written but I still got it wrong after many hours. A friend advised me not to edit a list while iterating. However, I'm still stuck.
def remove_first_two(list,n):
if list == []:
return []
else:
count = 0
for ele in list:
if ele == n:
list.remove(ele)
count += 1
if count == 2:
break
return list
list = [1,2,2,3]
print(remove_first_two(list,2)) => [1,2,3] instead of [1,3]
Use list.remove twice with try-except. That will delete first two entries. Complexity O(n)
list_a = [1,2,3,4]
try:
list_a.remove(n)
list_a.remove(n)
# run a loop too, if it's more than 2
except:
pass
You can try find all indexes and del:
a = [1,2,3,2,3,2,4]
indices = [i for i, x in enumerate(a) if x == 2]
print(indices)
[1, 3, 5]
del a[indices[0]], a[indices[1]]
print(a)
[1, 3, 2, 2, 4]
First, don't use 'list' as its a key word in Python. Use something else, like 'alist'.
The code below does what you want and keeps the basic form of what you already have. You can of course also use the built-in .remove() method.
def remove_first_two(alist, n):
if alist == []:
return []
else:
count = 0
while count < 2:
for ele in alist:
if ele == n:
alist.remove(ele)
count += 1
return alist
alist = [1,2,2,3]
print(remove_first_two(alist,2)) # Output -> [1,3]
When your friend says "do not edit a list while iterating," he/she is right, and what he/she means is that you should create another list all together. What you are looking to do is the following:
def remove_first_two(list, n):
if list == []:
return []
else:
new_list = []
count = 0
for ele in list:
if ele == n:
if count >= 2:
new_list.append(ele)
count += 1
else:
new_list.append(ele)
return new_list
However, note that you can use use some built in functions to make your life much easier:
list.remove(x)
Remove the first item from the list whose value is equal to x. It raises a ValueError if there is no such item.
Therefore, you can more simply do:
def remove_first_two(list, n):
if list == []:
return []
for _ in range(2):
if n in list:
list.remove(n)
return list
Python updates the list if you change it while iterating.
In you test case with list = [1,2,2,3] when list[1] is deleted and Python updates list = [1,2,3]. Now Python understands you have iterated till index 1 and continues from index 2 which now contains 3. So Python encounters only one occurance of 2.
So heed your friends advice and do not edit list while iterating :)
Now you can use Python's in-built list.remove(element) to delete first ocuurence of a element. Repeat it 2 times for desired output.
Also O(n) with a single parse.
def remove_first_two(mylist,n):
counter = 0
def myfilter (i):
nonlocal counter,n
if counter > 2:
return True
else:
counter += 1
return (i != n)
return (list(filter(myfilter,mylist)))
This can also be done in python 3.8 using assignment expressions in a list comprehension:
data = [1,2,3,2,3,2,4]
count = 2
num = 2
[x for x in data if x != num or (count:=count-1) < 0]
Results:
[1, 3, 2, 2, 4]
Here is the reason why your program does not work:
When you remove an element, the for loop moves on to the next element, but by "moving on" it is actually skipping the element which now occupies the position of the deleted element. It skips the element right after the one you deleted.
The correct way to iterate over a list while you delete elements is making index progression explicit, by using a while loop instead of a for loop, and not increase the index when you delete an element:
i = 0
while i < len(my_list):
if condition:
my_list.pop(i)
else:
i += 1
However, none of this is necessary in your case! Notice that when you use my_list.remove(ele), you are not providing an index as you would with my_list.pop(i), so Python has to search for the first element that matches ele. Although remove will be slower than pop when used by themselves, here remove allows you not use any loops at all, simply do my_list.remove(n) twice!
Last touch: If your list has less than two elements matching n, one of the two my_list.remove(n) commands would return a ValueError. You can account for this exception, knowing that if it happens, your list is ready and requires no further action.
So the code you need is:
try:
my_list.remove(n)
my_list.remove(n)
except ValueError:
pass
This question already has answers here:
Modifying list while iterating [duplicate]
(7 answers)
Closed 5 years ago.
Text = input('please enter your text')
l = [str(x) for x in Text.split()]
count = 0
for item in l:
for i in range(1,len(item)):
if item[i-1] == item[i]:
count +=1
if count <1:
l.remove(item)
count = 0
print (l)
the goal is : if we have a text : 'baaaaah mooh lpo mpoo' to get a list with elements they have 2 successive same characters, in this case ['baaaaah', 'mooh', 'mpoo' ]
the program is working with the mentioned example
if i use this one it's not working : 'hgj kio mpoo'
thank you
(complex)One liner:
>>> def successive_items(s):
return [x for x in s.split() if any(x[i]==x[i-1] for i in range(1,len(x)))]
>>> successive_items('baaaaah mooh lpo mpoo')
['baaaaah', 'mooh', 'mpoo']
>>> successive_items('hgj kio mpoo')
['mpoo']
In case of your code, you should not modify the list you are iterating over. Say, for example, lets have an array:
a = [1,2,3,4,5]
Now, if you iterate and remove elements (inside the loop), you would expect a to be empty. But let's check out:
>>> a = [1,2,3,4,5]
>>> for item in a:
a.remove(item)
>>> a
[2, 4]
See? It is not empty. This is better explained here.
Your program is not working for the second case because of list modification. Here is how:
Initially your list had ['hgj','kio','mpoo'].
After reading the first element you removed hgj. So the list now becomes ['kio','mpoo'].
The loop iterates the 2nd element next, and it gets mpoo (in the modified list);
kio was never read.
This might help:
sentence = input("please enter your text")
words = sentence.split()
answers = []
for each_word in words:
for idx in range(1, len(each_word)):
if each_word[idx] == each_word[idx-1]:
answers.append(each_word)
break
print(answers)
In your code, you are iterating over a list and at the same time you are modifying that very same list(by deleting elements from it), for more explanation see this answer
This question already has answers here:
Shuffling a list of objects
(25 answers)
Closed 8 years ago.
I am new with python. coming from a C++ background I'm ironically having trouble understanding the simplicity of this language not to mention how IDLE works.
anyhow I need to write a simple function that takes a list and returns a new list with the elements inside it shuffled
this is what I have so far
import random
def shuffle():
aList = []
i = 0
for i in range(0, 5):
elements = input(": ")
aList.append(elements)
shuffleList = random.shuffle(aList)
return shuffleList
shuffle()
and after I enter the elements(numerical numbers in this case), nothing outputs.. So the shuffleList for some reason is not being shown in there. Any ideas ?
>>>
: 1
: 2
: 3
: 4
: 5
>>>
random.shuffle shuffles the list in place, and its output is None.
Since you store and return its output in shuffleList = random.shuffle(aList), nothing is printed.
So instead, return the aList back:
import random
def shuffle():
aList = []
i = 0
for i in range(0, 5):
elements = input(": ")
aList.append(elements)
random.shuffle(aList)
return aList
random.shuffle is an in-place operation, it does not return anything
>>> l
[0, 1, 2, 3, 4]
>>> random.shuffle(l)
>>> l
[0, 3, 1, 4, 2]
Shuffle shuffles the list, it doesn't return a new list. Try this:
import random
def shuffle():
aList = []
for i in range(0, 5):
elements = input(": ")
aList.append(elements)
random.shuffle(aList)
return aList
print shuffle()
random.shuffle works in place, meaning it updates the provided parameter, rather than returning a value (other than the default, None). In C++, you would need to pass a reference to the list instead (e.g. int** aList), but in Python this distinction doesn't really exist.
So, you're actually setting shuffleList to None, and then returning it!
You can confirm this by using print to actually show the results when you're finished - just returning the value from the function won't output anything, as far as I am aware:
results = shuffle()
print results
To fix it, just return aList, instead of storing the result in another variable:
random.shuffle(aList)
return aList
random.shuffle() reorders the list in-place and returns None.
I'm currently developing a program in python and I just noticed that something was wrong with the foreach loop in the language, or maybe the list structure. I'll just give a generic example of my problem to simplify, since I get the same erroneous behavior on both my program and my generic example:
x = [1,2,2,2,2]
for i in x:
x.remove(i)
print x
Well, the problem here is simple, I though that this code was supposed to remove all elements from a list. Well, the problem is that after it's execution, I always get 2 remaining elements in the list.
What am I doing wrong? Thanks for all the help in advance.
Edit: I don't want to empty a list, this is just an example...
This is a well-documented behaviour in Python, that you aren't supposed to modify the list being iterated through. Try this instead:
for i in x[:]:
x.remove(i)
The [:] returns a "slice" of x, which happens to contain all its elements, and is thus effectively a copy of x.
When you delete an element, and the for-loop incs to the next index, you then skip an element.
Do it backwards. Or please state your real problem.
I think, broadly speaking, that when you write:
for x in lst:
# loop body goes here
under the hood, python is doing something like this:
i = 0
while i < len(lst):
x = lst[i]
# loop body goes here
i += 1
If you insert lst.remove(x) for the loop body, perhaps then you'll be able to see why you get the result you do?
Essentially, python uses a moving pointer to traverse the list. The pointer starts by pointing at the first element. Then you remove the first element, thus making the second element the new first element. Then the pointer move to the new second – previously third – element. And so on. (it might be clearer if you use [1,2,3,4,5] instead of [1,2,2,2,2] as your sample list)
Why don't you just use:
x = []
It's probably because you're changing the same array that you're iterating over.
Try Chris-Jester Young's answer if you want to clear the array your way.
I know this is an old post with an accepted answer but for those that may still come along...
A few previous answers have indicated it's a bad idea to change an iterable during iteration. But as a way to highlight what is happening...
>>> x=[1,2,3,4,5]
>>> for i in x:
... print i, x.index(i)
... x.remove(i)
... print x
...
1 0
[2, 3, 4, 5]
3 1
[2, 4, 5]
5 2
[2, 4]
Hopefully the visual helps clarify.
I agree with John Fouhy regarding the break condition. Traversing a copy of the list works for the remove() method, as Chris Jester-Young suggested. But if one needs to pop() specific items, then iterating in reverse works, as Erik mentioned, in which case the operation can be done in place. For example:
def r_enumerate(iterable):
"""enumerator for reverse iteration of an iterable"""
enum = enumerate(reversed(iterable))
last = len(iterable)-1
return ((last - i, x) for i,x in enum)
x = [1,2,3,4,5]
y = []
for i,v in r_enumerate(x):
if v != 3:
y.append(x.pop(i))
print 'i=%d, v=%d, x=%s, y=%s' %(i,v,x,y)
or with xrange:
x = [1,2,3,4,5]
y = []
for i in xrange(len(x)-1,-1,-1):
if x[i] != 3:
y.append(x.pop(i))
print 'i=%d, x=%s, y=%s' %(i,x,y)
If you need to filter stuff out of a list it may be a better idea to use list comprehension:
newlist = [x for x in oldlist if x%2]
for instance would filter all even numbers out of an integer list
The list stored in the memory of a computer. This deals with the pointer to a memory artifact. When you remove an element, in a by-element loop, you are then moving the pointer to the next available element in the memory address
You are modifying the memory and iterating thru the same.
The pointer to the element moves through the list to the next spot available.
So in the case of the Size being 5...enter code here
[**0**,1,2,3,4]
remove 0 ---> [1,**2**,3,4] pointer moves to second index.
remove 2 ---> [1,3,**4**] pointer moves to 3rd index.
remove 4 ---> [1,3]
I was just explaining this to my students when they used pop(1). Another very interesting side-effect error.
x=[1,**2**,3,4,5]
for i in x:
x.pop(1)
print(x,i)
[1, **3**, 4, 5] 1 at index 0 it removed the index 1 (2)
[1, **4**, 5] 3 at index 1 it removed the index 1 (3)
[1, 5] 5 at index 2 it removed the index 1 (4)
heh.
They were like why isnt this working... I mean... it did... exactly what you told it to do. Not a mind reader. :)