47

To find the index of an item in a list, you use:

list.index(x)
Return the index in the list of the first item whose value is x. 
It is an error if there is no such item.

That seems a little odd to me, that it would throw an error if the item wasn't found. Where I come from (Objective-C land), it returns a NSNotFound enum (which is just a max int, indicating the item wasn't found).

So I made something ugly to go around this:

index = 0
for item in self.items:
   if item.id == desired_id:
        return index
    index = index + 1
 return -1

I used -1 to indicate the item wasn't found. What's a better way to do this, and why doesn't Python have something like this built in?

5
  • I don't know if there's something built in, but in your workaround you can use the in operator instead of a for. Check if an item is in the list, and then use index(). Commented Oct 31, 2012 at 14:48
  • 1
    @Gil: That basically searches the list twice. Not very efficient with very long lists. Commented Oct 31, 2012 at 14:49
  • 2
    @Junuxx you are wrong. Yes it scans the list twice but it's at "C level". It's still a lot faster than iterating over the list from python.(Obviously it would be even faster to call index and handle the error) Commented Oct 31, 2012 at 14:50
  • 7
    @Bakuriu: That's a funny way to say I'm right. Commented Oct 31, 2012 at 14:52
  • 1
    @Junuxx I've clarified that in an answer, so that the OP can see himself the difference between his approach, the solutions already proposed and Gil's advice. Commented Oct 31, 2012 at 15:09

6 Answers 6

44
a = [1]
try:
    index_value = a.index(44)
except ValueError:
    index_value = -1

How about this?

Sign up to request clarification or add additional context in comments.

12 Comments

ValueError: 44 is not in list
@JakobBowyer silly question, but what is the scope of the variable index_valeu in the try statement? Can it still be used outside the try statement, or must it first be declared on top?
This is the way to do something like this (+1). However, I wouldn't use -1 as that's a valid python index. I also would spell it index_value instead of index_valeu :-D
@mohabitar -- The scope is that same as it would be if that was an if-else statement. In other words, it is available outside the try/except clause.
@krupeshAnadkat in only works if you have the original value, not an index
|
18

I agree with the general solution that was pointed out, but I'd like to look a bit more into the approaches that were explained in the answers and comments to see which one is more efficient and in which situations.

First of all, the three basic approaches:

>>> def my_index(L, obj):
...     for i, el in enumerate(L):
...             if el == obj:
...                     return i
...     return -1
... 
>>> def my_index2(L, obj):
...     try:
...             return L.index(obj)
...     except ValueError:
...             return -1
... 
>>> def my_index3(L, obj):
...     if obj in L:
...             return L.index(obj)
...     return -1
... 

The first and second solutions scan the list only once, and so you may think that they are faster than the third one because it scans the list twice. So let's see:

>>> timeit.timeit('my_index(L, 24999)', 'from __main__ import my_index, L', number=1000)
1.6892211437225342
>>> timeit.timeit('my_index2(L, 24999)', 'from __main__ import my_index2, L', number=1000)
0.403195858001709
>>> timeit.timeit('my_index3(L, 24999)', 'from __main__ import my_index3, L', number=1000)
0.7741198539733887

Well the second is really the fastest, but you can notice that the first one is much slower than the third one, even though it scans the list only once. If we increase the size of the list things does not change much:

>>> L = list(range(2500000))
>>> timeit.timeit('my_index(L, 2499999)', 'from __main__ import my_index, L', number=100)
17.323430061340332
>>> timeit.timeit('my_index2(L, 2499999)', 'from __main__ import my_index2, L', number=100)
4.213982820510864
>>> timeit.timeit('my_index3(L, 2499999)', 'from __main__ import my_index3, L', number=100)
8.406487941741943

The first one is still 2x times slower.

and if we search something that it's not in the list things get even worse for the first solution:

>>> timeit.timeit('my_index(L, None)', 'from __main__ import my_index, L', number=100)
19.055058002471924
>>> timeit.timeit('my_index2(L, None)', 'from __main__ import my_index2, L', number=100)
5.785136938095093
>>> timeit.timeit('my_index3(L, None)', 'from __main__ import my_index3, L', number=100)
5.46164608001709

As you can see in this case the third solution beats even the second one, and both are almost 4x faster than the python code. Depending on how often you expect the search to fail you want to choose #2 or #3(even though in 99% of the cases number #2 is better).

As a general rule, if you want to optimize something for CPython then you want to do as much iterations "at C level" as you can. In your example iterating using a for loop is exactly something you do not want to do.

1 Comment

Good point, but it is kind of sad that Python is so slow.
13

It's not a good idea to return -1 as that is a valid index in Python (see Python list.index throws exception when index not found).

Probably best to catch the index error and act accordingly.

1 Comment

But you can still return None if not found, which is meaningful and won't be confused with an index.
1

use exception-handling, list.index raises ValueError so you can catch that exception:

A simple example:

In [78]: lis=[1,2,3,4]

In [79]: for i in range(-1,6):
    try:
        print lis.index(i)
    except ValueError:    
        print i,"not found"

-1 not found
0 not found
0
1
2
3
5 not found

Comments

1

There is a clear reason for this behavior:

>>> import this
...
In the face of ambiguity, refuse the temptation to guess.
...

There is no clear interpretation on how the system should respond to an object like "NSNotFound", so you must refuse to guess, and then it became useless to implement a special feature for that.

think what happen if I try to do something like this:

[ objective.index(i)+1 for i in reference_list ]

What does it mean to add 1 to the NSNotFound? isn't it simpler to do something like:

[ objective.index(i)+1 for i in reference_list if i in objective ]

And, -1 is actually a valid index for a list, meaning "take the last value", so if you try to use it as a special error code it's very plausible that you are going to end up into some nasty, nasty bug.

Guido has a very strong sense of design, don't underestimate him ;)

If, that said, you still need something like that, you can try with this code:

class NotFoundError(Exception):
    def __init__(self,container,index):
        self.message = "object "+str(index)+" not found on "+str(container)
        self.container = container
        self.index = index
    def __str__(self):
        return self.message

def getindex(cont,idx):
    try:
        return cont.index(idx)
    except:
        return NotFoundError(cont,idx)

a = [1,2]

print getindex(a,3)
#object 3 not found on [1, 2]

2 Comments

I think there are plenty of counter examples from other programming languages for other ways to do this. For one thing, you could return None instead of -1. Furthermore, although -1 is a valid index, you only need to document that the index() function only returns positive indexes. Too late now of course -- the design decision has been made.
This is a basic thing that any language should do. Producing an error for such a common case is simply wrong. Why isn't there an error produced when 'a' in list? It's not consistent behavior.
0

It's better to think of it as 'raising an exception' than 'throwing an error'.

Exceptions in Python are not just for errors, they are for exceptional circumstances - hence the name. If list.index() had returned some special value, it would need to be one which

  1. could not have been returned had list.index() found the item

  2. could not subsequently be misinterpreted by naïve code.

The first condition excludes all positive integers (including zero and sys.maxint), and the second excludes negative ones too (because negative indexes are a valid way to index into a list in Python). Anything other than an integer is likely to raise an exception later anyway, if the subsequent code assumes that's what it's going to get.

Regardless of whether the method raises an exception or returns a special value, you're often going to need to do something with that information, and this:

try:
    index = list.index(x)
except ValueError:
    # do something

is more readable than this:

index = list.index(x)
if index == some_special_value:
    # do something

... and in the latter case, forgetting to guard against the exceptional circumstance would cause the code to fail silently, probably leading to confusing errors elsewhere in the code.

Worse, you'd have to remember or look up what that special value is, for this and any other methods or functions that behave like that.

4 Comments

Unfortunately the rationales you give for why it's this way are completely busted by the fact that strings have a find() method that returns -1 when the substring isn't found. There's no reason list couldn't have a similar method; it just doesn't.
Actually that make me think that is the find method of the string that is broken, not the other way around
str.find() is something of an anomaly in Python - perhaps even a wart - and we also have str.index(), which behaves as expected. One exception to the way Python usually does things (and one whose removal from the language has been suggested precisely on the grounds that it isn't pythonic) doesn't invalidate the advice above.
I don't find the first case more readable than the second. If you find conditional expressions hard to read, then you probably are not a very experienced programmer and you will probably find try/except statements even harder to understand.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.