Python index of item in list without error?

Question:

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?

Asked By: Snowman

||

Answers:

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.

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

How about this?

Answered By: Jakob Bowyer

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
Answered By: Ashwini Chaudhary

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.

Answered By: Bakuriu

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]
Answered By: EnricoGiampieri

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.

Answered By: Zero Piraeus
Categories: questions Tags:
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.