Reputation: 1742
Lately I learned about lists and for loops, as well as the command .pop()
that indicates and removes the last item in a list.
So I tried to write a code to remove the last items in a list one by one, until it remains with only one item.
The code is:
list_A = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j']
for i in list_A:
print(list_A.pop())
if 'c' not in list_A:
break
print("job done.")
The output of python 3.6 gives me this:
/Library/Frameworks/Python.framework/Versions/3.6/bin/python3.6
j
i
h
g
f
job done.
As you can see, it actually worked, but for a half of it?
I was expecting:
j
i
h
g
f
e
d
c
job done
I mean, I will be more comfortable if it returns some error, that means the code is not right. But why did it work, but not a full way through?
Upvotes: 5
Views: 12477
Reputation: 22776
list_A = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j']
while list_A: # while list_A has elements, in case 'c' wasn't present
el = list_A.pop() # save the last element
print(el)
if 'c'==el: # if 'c' was popped (reached)
break
print("job done.")
This way, even if 'c'
isn't present, it'll just print everything then exit. This also avoids checking if 'c'
is present on every iteration, which takes time.
Based on @MSeifert's comment, if the loop shouldn't stop at the first c
popped instead stops whenever the list has no c
, a little modification to the code above results in:
list_A = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'c', 'h', 'i', 'j']
while list_A:
print(list_A.pop())
if 'c' not in list_A:
break
print("job done.")
We could go faster, but I don't know wether you learned list slicing and comprehesion yet, so here's a better and faster solution:
list_A = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j']
try:
p=list_A.index('c')
r='\n'.join(list_A[list_A.index('c'):][::-1])
except ValueError:
r='\n'.join(list_A[::-1])
print(r)
print('job done.')
Upvotes: 1
Reputation: 87084
You are mutating the list whilst iterating over it.
You can use a while
loop to do this:
list_A = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j']
while 'c' in list_A:
print(list_A.pop())
print('job done')
Output:
j i h g f e d c job done
A more efficient way would be to determine the index of the first instance of the sentinel character, and remove it and the rest of the list (although the characters are not printed as they are removed):
try:
pos = list_A.index('c')
list_A[:] = list_A[:pos]
# del list_A[pos:] # more efficient alternative suggested by @ShadowRanger
except ValueError as e:
pass
Upvotes: 6
Reputation: 152657
I recently answered a similar question and it boils down to: Don't modify the sequence you're iterating over.
Using a custom iterator (from another answer of mine) shows what happened:
class CustomIterator(object):
def __init__(self, seq):
self.seq = seq
self.idx = 0
def __iter__(self):
return self
def __next__(self):
print('give next element:', self.idx)
for idx, item in enumerate(self.seq):
if idx == self.idx:
print(idx, '--->', item)
else:
print(idx, ' ', item)
try:
nxtitem = self.seq[self.idx]
except IndexError:
raise StopIteration
self.idx += 1
return nxtitem
next = __next__ # py2 compat
list_A = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j']
for i in CustomIterator(list_A):
print(list_A.pop())
if 'c' not in list_A:
break
Which prints:
give next element: 0
0 ---> a
1 b
2 c
3 d
4 e
5 f
6 g
7 h
8 i
9 j
j
give next element: 1
0 a
1 ---> b
2 c
3 d
4 e
5 f
6 g
7 h
8 i
i
give next element: 2
0 a
1 b
2 ---> c
3 d
4 e
5 f
6 g
7 h
h
give next element: 3
0 a
1 b
2 c
3 ---> d
4 e
5 f
6 g
g
give next element: 4
0 a
1 b
2 c
3 d
4 ---> e
5 f
f
give next element: 5
0 a
1 b
2 c
3 d
4 e
So it didn't end because of the break
but because it iterated over the whole list (or better: until there were no more items!).
Also the 'c' not in listA
is an O(n)
operation, so your loop is effectively O(n**2)
. Why not just find the first index of 'c'
and simply iterate until you're there:
list_A = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j']
try:
c_index = list_A.index('c')
except ValueError:
# no 'c' in the list, probably should do something more useful here ...
pass
else:
for item in reversed(list_A[c_index:]): # print the items
print(item)
del list_A[c_index:] # remove the items from the list
prints (as expected):
j
i
h
g
f
e
d
c
Upvotes: 2
Reputation: 16496
When using a for..in
loop in Python, you are not supposed to modify the list.
What happened here is the following:
a
pop()
removes the last list entry, so in that first loop iteration you get rid of the last letter j
and print ite
you remove and print f
from the lista
to e
and since you just iterated over e
the loop's job is doneIt's really hard to say what you wanted to do here, since it's more of playing around rather than getting something done. I would suggest to use a while
loop though, whenever you intend to edit the list from within the loop. Your example with proper semantics could look like this:
while list_A:
print(list_A.pop())
if "c" not in list_A:
break
This loop goes for as long as there is items in the list and only stops once there is no c
in the list anymore.
Upvotes: 3