3

This is the most common problem I face while trying to learn programming in python. The problem is, when I try to iterate a list using "range()" function to check if given item in list meets given condition and if yes then to delete it, it will always give "IndexError". So, is there a particular way to do this without using any other intermediate list or "while" statement? Below is an example:

l = range(20)
for i in range(0,len(l)):
  if l[i] == something:
    l.pop(i)
BenMorel
  • 34,448
  • 50
  • 182
  • 322
Alinwndrld
  • 777
  • 2
  • 6
  • 13

6 Answers6

7

First of all, you never want to iterate over things like that in Python. Iterate over the actual objects, not the indices:

l = range(20)
for i in l:
    ...

The reason for your error was that you were removing an item, so the later indices cease to exist.

Now, you can't modify a list while you are looping over it, but that isn't a problem. The better solution is to use a list comprehension here, to filter out the extra items.

l = range(20)
new_l = [i for i in l if not i == something]

You can also use the filter() builtin, although that tends to be unclear in most situations (and slower where you need lambda).

Also note that in Python 3.x, range() produces a generator, not a list.

It would also be a good idea to use more descriptive variable names - I'll presume here it's for example, but names like i and l are hard to read and make it easier to introduce bugs.

Edit:

If you wish to update the existing list in place, as pointed out in the comments, you can use the slicing syntax to replace each item of the list in turn (l[:] = new_l). That said, I would argue that that case is pretty bad design. You don't want one segment of code to rely on data being updated from another bit of code in that way.

Edit 2:

If, for any reason, you need the indices as you loop over the items, that's what the enumerate() builtin is for.

Gareth Latty
  • 86,389
  • 17
  • 178
  • 183
  • That assumes you only have one name referring to the list. – David Heffernan May 10 '12 at 17:48
  • @DavidHeffernan You can reassign the new list to the old one, if you want. – Gareth Latty May 10 '12 at 17:48
  • Not if you have other other references in code that is out of your immediate control. A little far fetched I know, but sometimes in-place modification is needed. – David Heffernan May 10 '12 at 17:52
  • @DavidHeffernan, so in that case use a slice-assignment to update the list in-place. – Duncan May 10 '12 at 17:53
  • @DavidHeffernan Then you can use the slicing syntax to replace each item of the list in turn (`l[:] = new_l`). That said, I would argue that that case is pretty bad design. You don't want one segment of code to rely on data being updated from another bit of code in that way. – Gareth Latty May 10 '12 at 17:53
  • @Lattyware I thought that, but what if sometime I need Index no. to refer to other item in list with respect to current item? – Alinwndrld May 10 '12 at 17:56
  • @Alinwndrld I don't know what you mean. That question seems unrelated to my point. – David Heffernan May 10 '12 at 17:56
  • @Alinwndrld If you need the index of an item in the list while looping over it, use [the `enumerate()` builtin](http://docs.python.org/library/functions.html#enumerate). – Gareth Latty May 10 '12 at 17:57
  • @DavidHeffernan I meant that for Lattyware. – Alinwndrld May 10 '12 at 17:58
  • @Alinwndrld You can do what you ask, but you need to loop over the list in reverse order I think. – David Heffernan May 10 '12 at 17:59
  • @DavidHeffernan What reason is there to loop over the list in reverse? – Gareth Latty May 10 '12 at 18:02
  • So you can delete items from the list without screwing up your iteration. – David Heffernan May 10 '12 at 18:03
  • @DavidHeffernan Modifying a list while you iterate over it is a bad idea, and fragile. It's a better solution to use a list comprehension (or a generator expression if needs be). – Gareth Latty May 10 '12 at 18:06
  • We are going round in circles. What you say is true if creating a new list object is acceptable. If you have to modify in place then walking the list in reverse is an old an venerable trick. – David Heffernan May 10 '12 at 18:08
  • There is no reason to believe that making a new list is a problem here, and if it is, using a generator expression (so only one item is evaluated at a time) is a much better solution than iterating over the list in reverse. – Gareth Latty May 10 '12 at 18:10
  • How does a generator expression modify a list object? – David Heffernan May 10 '12 at 18:12
  • It doesn't, but it means you don't have to create a new list, so you don't need any more memory than you were already using for your existing list - the only other reason to modify in-place is a bad one. – Gareth Latty May 10 '12 at 18:17
2

You can always do this sort of thing with a list comprehension:

newlist=[i for i in oldlist if not condition ]
mgilson
  • 300,191
  • 65
  • 633
  • 696
0

You should look the problem from the other side: add an element to a list when it is equal with "something". with list comprehension:

l = [i for i in xrange(20) if i != something]
0
  • you should not use for i in range(0,len(l)):, use for i, item in enumerate(l): instead if you need the index, for item in l: if not
  • you should not manipulate a structure you are iterating over. when faced to do so, iterate over a copy instead
  • don't name a variable l (may be mistaken as 1 or I)
  • if you want to filter a list, do so explicitly. use filter() or list comprehensions

BTW, in your case, you could also do:

while something in list_: list_.remove(something)

That's not very efficient, though. But depending on context, it might be more readable.

ch3ka
  • 11,792
  • 4
  • 31
  • 28
0

As others have said, iterate over the list and create a new list with just the items you want to keep.

Use a slice assignment to update the original list in-place.

l[:] = [item for item in l if item != something]
Duncan
  • 92,073
  • 11
  • 122
  • 156
0

The reason you're getting an IndexError is because you're changing the length of the list as you iterate in the for-loop. Basically, here's the logic...

#-- Build the original list: [0, 1, 2, ..., 19]
l = range(20)

#-- Here, the range function builds ANOTHER list, in this case also [0, 1, 2, ..., 19]
#-- the variable "i" will be bound to each element of this list, so i = 0 (loop), then i = 1 (loop), i = 2, etc.
for i in range(0,len(l)):
    if i == something:
        #-- So, when i is equivalent to something, you "pop" the list, l.
        #-- the length of l is now *19* elements, NOT 20 (you just removed one)
        l.pop(i)
    #-- So...when the list has been shortened to 19 elements...
    #-- we're still iterating, i = 17 (loop), i = 18 (loop), i = 19 *CRASH*
    #-- There is no 19th element of l, as l (after you popped out an element) only
    #-- has indices 0, ..., 18, now.

NOTE also, that you're making the "pop" decision based on the index of the list, not what's in the indexed cell of the list. This is unusual -- was that your intention? Or did you mean something more like...

if l[i] == something:
    l.pop(i)

Now, in your specific example, (l[i] == i) but this is not a typical pattern.

Rather than iterating over the list, try the filter function. It's a built-in (like a lot of other list processing functions: e.g. map, sort, reverse, zip, etc.)

Try this...

#-- Create a function for testing the elements of the list.
def f(x):
    if (x == SOMETHING):
        return False
    else:
        return True

#-- Create the original list.
l = range(20)

#-- Apply the function f to each element of l.
#-- Where f(l[i]) is True, the element l[i] is kept and will be in the new list, m.
#-- Where f(l[i]) is False, the element l[i] is passed over and will NOT appear in m.
m = filter(f, l)

List processing functions go hand-in-hand with "lambda" functions - which, in Python, are brief, anonymous functions. so, we can re-write the above code as...

#-- Create the original list.
l = range(20)

#-- Apply the function f to each element of l.
#-- Where lambda is True, the element l[i] is kept and will be in the new list, m.
#-- Where lambda is False, the element l[i] is passed over and will NOT appear in m.
m = filter(lambda x: (x != SOMETHING), l)

Give it a go and see it how it works!

parselmouth
  • 1,598
  • 8
  • 8
  • Using `filter()` and a `lambda` is slower and less clear than a list comprehension here. – Gareth Latty May 10 '12 at 18:03
  • @Lattyware agreed. I +1'd the list comprehension solution, above. My only caveat in this case is that for a lot of beginners, list comprehension is actually LESS clear than a more verbose approach. Otherwise, I'm all for it. – parselmouth May 10 '12 at 18:06
  • As with all language constructs, they need to be explained before they can be used, yes, but I don't think that means they should ever be avoided. – Gareth Latty May 10 '12 at 18:09
  • Yeah I meant l[i] instead of "i", edited it. – Alinwndrld May 10 '12 at 18:11