73
l  = range(100)                         
for i in l:                         
    print i,                         
    print l.pop(0),                  
    print l.pop(0)

The above python code gives the output quite different from expected. I want to loop over items so that I can skip an item while looping.

Please explain.

Xolve
  • 22,298
  • 21
  • 77
  • 125

7 Answers7

75

Never alter the container you're looping on, because iterators on that container are not going to be informed of your alterations and, as you've noticed, that's quite likely to produce a very different loop and/or an incorrect one. In normal cases, looping on a copy of the container helps, but in your case it's clear that you don't want that, as the container will be empty after 50 legs of the loop and if you then try popping again you'll get an exception.

What's anything BUT clear is, what behavior are you trying to achieve, if any?! Maybe you can express your desires with a while...?

i = 0
while i < len(some_list):
    print i,                         
    print some_list.pop(0),                  
    print some_list.pop(0)
maxywb
  • 2,275
  • 1
  • 19
  • 25
Alex Martelli
  • 854,459
  • 170
  • 1,222
  • 1,395
  • 2
    Wait should you increment i in the loop? – Snowman May 20 '12 at 17:10
  • 6
    @maq that's not necessary. `pop` actually removes the element. So you keep looking at element `0` and popping it. Works as intended. – ashes999 Mar 01 '13 at 22:25
  • Alex, I found that this works on trivial examples, should we still avoid this? e.g. >>> l = list('abcdefab') >>> for i in l: if l.count(i) > 1: l.remove(i) – Russia Must Remove Putin Feb 24 '14 at 01:58
  • *because iterators on that container are not going to be informed of your alterations* : Could you please explain why? – haccks Mar 04 '15 at 17:16
  • 1
    @haccks, because a container doesn't even keep track of iterators that are out on it, much less hook even altering-method to loop over every such iterator and somehow magically let each iterator know about the alterations. It would be a lot subtle, complex code, and checks slowing down very frequent operations. Try coding a list-like container such that, e.g, `for i, x in enumerate(container):` works perfectly as the loop's body selectively removes some of the container's items, and you'll get a better grasp of the issues -- **nested** loops next, and, next again, ...:-) – Alex Martelli Mar 04 '15 at 18:59
  • That means once the iterator is yield by the evaluation of `l` (in OP's code) then any modification to `l` is not informed to the iterator. This is what [`for`](https://docs.python.org/2/reference/compound_stmts.html#for) loop reference also says. But when I try to run this code `for w in words: print w if len(w) < 6: words.remove(w) print words` it gives the output `cat defenestrate ['window', 'defenestrate']`. Where has `'window'` gone? Seems that iterator is informed about the alteration to `words`. – haccks Mar 05 '15 at 08:47
  • @haccks, the `words` list "slipped one to the left" with each deletion, the iterator was not informed thus kept its internal index into `words`, then incremented it for the next leg of the loop. So the effect (the way this undefined behavior actually plays out in practice in this case) is that some items of the list happen to be "skipped" in the iteration. Once more: roll your own iterator-on-list class and you'll grasp it better. – Alex Martelli Mar 05 '15 at 14:45
  • *the words list "slipped one to the left" with each deletion, ...*: Of course that must be the case. But the internal counter/index will keep track of the iterator's items, not of the list items, AFAIK. Am I wrong? – haccks Mar 05 '15 at 16:12
  • 5
    The iterator doesn't own nor hold any items -- it reaches into the list (at the iterator's current index -- *that* index is the one thing the iterator owns and holds) each time it needs to supply a value for `next` (and also increments its internal iterator after that). – Alex Martelli Mar 05 '15 at 16:14
  • Then what is the meaning of this line: [*The expression list is evaluated once;*](https://docs.python.org/2/reference/compound_stmts.html#for) ? (Actually I am kinda geek and also beginner to python.) – haccks Mar 05 '15 at 16:22
  • @haccks, SO is starting to scold for "extended discussions in comments", so **please** open a new Q if you're burning to know about this -- can't keep chatting in comments on this one, and I have no time for chat rooms. The sentence you quote means exactly what it says: the expression list IS evaluated exactly once. It does *NOT* mean any copying occurs -- references to the underlying list object (which is mutable but shouldn't be mutated during the loop) reflect whatever changes you make to that list object, right or wrong ones as it may be. – Alex Martelli Mar 05 '15 at 18:36
  • OK. Then I understood that excerpt wrong which means that the iterable object is evaluated only once to create an *iterator* on it because the `for` statement calls `iter()` on the container object. This iterator object pulls out the list items one by one using `next()` . Any modification to the list will affect this pulling sequence. Your last comment about iterator was really helpful. Thanks for your time. – haccks Mar 05 '15 at 19:00
50

I've been bitten before by (someone else's) "clever" code that tries to modify a list while iterating over it. I resolved that I would never do it under any circumstance.

You can use the slice operator mylist[::3] to skip across to every third item in your list.

mylist = [i for i in range(100)]
for i in mylist[::3]:
    print(i)

Other points about my example relate to new syntax in python 3.0.

  • I use a list comprehension to define mylist because it works in Python 3.0 (see below)
  • print is a function in python 3.0

Python 3.0 range() now behaves like xrange() used to behave, except it works with values of arbitrary size. The latter no longer exists.

Eric
  • 95,302
  • 53
  • 242
  • 374
Ewan Todd
  • 7,315
  • 26
  • 33
19

The general rule of thumb is that you don't modify a collection/array/list while iterating over it.

Use a secondary list to store the items you want to act upon and execute that logic in a loop after your initial loop.

Paul Sasik
  • 79,492
  • 20
  • 149
  • 189
16

Use a while loop that checks for the truthfulness of the array:

while array:
    value = array.pop(0)
    # do some calculation here

And it should do it without any errors or funny behaviour.

Eugene Eeo
  • 161
  • 1
  • 2
9

Try this. It avoids mutating a thing you're iterating across, which is generally a code smell.

for i in xrange(0, 100, 3):
    print i

See xrange.

Hank Gay
  • 70,339
  • 36
  • 160
  • 222
  • 7
    Python 3.0 range() now behaves like xrange() used to behave, except it works with values of arbitrary size. The latter no longer exists. – Ewan Todd Oct 28 '09 at 16:08
1

I guess this is what you want:

l  = range(100)  
index = 0                       
for i in l:                         
    print i,              
    try:
        print l.pop(index+1),                  
        print l.pop(index+1)
    except IndexError:
        pass
    index += 1

It is quite handy to code when the number of item to be popped is a run time decision. But it runs with very a bad efficiency and the code is hard to maintain.

LMCuber
  • 113
  • 1
  • 11
York Chang
  • 11
  • 2
-3

This slice syntax makes a copy of the list and does what you want:

l  = range(100)  
for i in l[:]:  
    print i,  
    print l.pop(0),  
    print l.pop(0)
thethinman
  • 322
  • 1
  • 6
  • 8
    I'm seeing this very late, but this answer is wrong. The code provided will crash after iterating over 50 items, since it's removing two items from the original list each time through the loop, but not skipping any in the sliced one. – Blckknght Jul 27 '12 at 23:41