45

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?

Cody Gray - on strike
  • 239,200
  • 50
  • 490
  • 574
Snowman
  • 31,411
  • 46
  • 180
  • 303
  • 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()`. – GilZ Oct 31 '12 at 14:48
  • 1
    @Gil: That basically searches the list twice. Not very efficient with very long lists. – Junuxx Oct 31 '12 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) – Bakuriu Oct 31 '12 at 14:50
  • 6
    @Bakuriu: That's a funny way to say I'm right. – Junuxx Oct 31 '12 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. – Bakuriu Oct 31 '12 at 15:09

6 Answers6

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

How about this?

Noel Evans
  • 8,113
  • 8
  • 48
  • 58
Jakob Bowyer
  • 33,878
  • 8
  • 76
  • 91
  • `ValueError: 44 is not in list` – Jakob Bowyer Oct 31 '12 at 14:50
  • @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? – Snowman Oct 31 '12 at 14:55
  • 1
    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 – mgilson Oct 31 '12 at 14:55
  • @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. – mgilson Oct 31 '12 at 14:56
  • @mgilson wait variables in an if-else statement are available outside the statement as well? Am I understanding that correct, because that's not the case with C or Objective-C – Snowman Oct 31 '12 at 14:57
  • I declared it for use in try, but notice I also declare it in except, by doing this the value is accessible one level above this scope – Jakob Bowyer Oct 31 '12 at 14:58
  • @mohabitar -- Yes. Python has module-level, class-level and function level scopes (I can't think of any others ...) Any variable defined within a function is available in that function (as long as you already executed the line where it was first defined). Try this: `if True: a = 2; print a` (it works) – mgilson Oct 31 '12 at 14:59
  • @mgilson what about if that was False? – Snowman Oct 31 '12 at 15:01
  • @mohabitar -- It would fail on a `NameError` because you never executed the line where `a` was defined. If you added an `else` clause which defined `a` as something, then it would work again. – mgilson Oct 31 '12 at 15:03
  • use of `in` operator works too, no try,exception required :) – krupesh Anadkat Jun 19 '21 at 06:57
  • 1
    @krupeshAnadkat `in` only works if you have the original value, not an index – Jakob Bowyer Jul 01 '21 at 19:09
  • @JakobBowyer thats true – krupesh Anadkat Jul 02 '21 at 04:03
15

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.

Bakuriu
  • 98,325
  • 22
  • 197
  • 231
12

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.

Community
  • 1
  • 1
doc
  • 765
  • 1
  • 6
  • 24
  • 3
    But you can still return None if not found, which is meaningful and won't be confused with an index. – tsh Mar 10 '21 at 10:36
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
Ashwini Chaudhary
  • 244,495
  • 58
  • 464
  • 504
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]
Community
  • 1
  • 1
EnricoGiampieri
  • 5,947
  • 1
  • 27
  • 26
  • 3
    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. – Christopher Barber Aug 11 '18 at 17:10
  • 1
    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. – Steve Gon Apr 23 '22 at 15:46
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.

Zero Piraeus
  • 56,143
  • 27
  • 150
  • 160
  • 8
    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. – kindall Oct 31 '12 at 15:45
  • 2
    Actually that make me think that is the find method of the string that is broken, not the other way around – EnricoGiampieri Oct 31 '12 at 15:54
  • 1
    `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. – Zero Piraeus Oct 31 '12 at 16:23
  • 1
    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. – Christopher Barber Aug 11 '18 at 17:16