132

The list.index(x) function returns the index in the list of the first item whose value is x.

Is there a function, list_func_index(), similar to the index() function that has a function, f(), as a parameter. The function, f() is run on every element, e, of the list until f(e) returns True. Then list_func_index() returns the index of e.

Codewise:

>>> def list_func_index(lst, func):
      for i in range(len(lst)):
        if func(lst[i]):
          return i
      raise ValueError('no element making func True')

>>> l = [8,10,4,5,7]
>>> def is_odd(x): return x % 2 != 0
>>> list_func_index(l,is_odd)
3

Is there a more elegant solution? (and a better name for the function)

3

9 Answers 9

175

You could do that in a one-liner using generators:

next(i for i,v in enumerate(l) if is_odd(v))

The nice thing about generators is that they only compute up to the requested amount. So requesting the first two indices is (almost) just as easy:

y = (i for i,v in enumerate(l) if is_odd(v))
x1 = next(y)
x2 = next(y)

Though, expect a StopIteration exception after the last index (that is how generators work). This is also convenient in your "take-first" approach, to know that no such value was found --- the list.index() function would raise ValueError here.

3
  • 15
    This isn't obfuscatory - or at least, it's not any more obfuscatory than using map(f, seq) instead of [f(x) for x in seq] is. In other words, it's idiomatic. And like other idioms, it's not straightforward until it's part of your vocabulary. Commented Nov 9, 2009 at 21:41
  • 4
    just a reminder to catch StopIteration if the end condition might not be met.
    – payala
    Commented Aug 30, 2018 at 18:37
  • 15
    Small hint: next accepts a second argument that will be returned in case of no match, instead of raising StopIteration.
    – bgusach
    Commented Apr 3, 2020 at 15:18
25

One possibility is the built-in enumerate function:

def index_of_first(lst, pred):
    for i, v in enumerate(lst):
        if pred(v):
            return i
    return None

It's typical to refer a function like the one you describe as a "predicate"; it returns true or false for some question. That's why I call it pred in my example.

I also think it would be better form to return None, since that's the real answer to the question. The caller can choose to explode on None, if required.

3
  • 1
    more elegant, better named, indeed
    – bandana
    Commented Nov 9, 2009 at 14:23
  • I think the OP wanted to emulate index's behavior of raising ValueError if the given value is not found.
    – PaulMcG
    Commented Nov 9, 2009 at 14:24
  • +1 for enumerate which is a big favorite of mine. I can't remember the last time I actually had to maintain an index variable the old-fashioned C way in python. Commented Nov 9, 2009 at 14:27
18

Paul's accepted answer is best, but here's a little lateral-thinking variant, mostly for amusement and instruction purposes...:

>>> class X(object):
...   def __init__(self, pred): self.pred = pred
...   def __eq__(self, other): return self.pred(other)
... 
>>> l = [8,10,4,5,7]
>>> def is_odd(x): return x % 2 != 0
... 
>>> l.index(X(is_odd))
3

essentially, X's purpose is to change the meaning of "equality" from the normal one to "satisfies this predicate", thereby allowing the use of predicates in all kinds of situations that are defined as checking for equality -- for example, it would also let you code, instead of if any(is_odd(x) for x in l):, the shorter if X(is_odd) in l:, and so forth.

Worth using? Not when a more explicit approach like that taken by @Paul is just as handy (especially when changed to use the new, shiny built-in next function rather than the older, less appropriate .next method, as I suggest in a comment to that answer), but there are other situations where it (or other variants of the idea "tweak the meaning of equality", and maybe other comparators and/or hashing) may be appropriate. Mostly, worth knowing about the idea, to avoid having to invent it from scratch one day.

8
  • Nice one! But what would we "name" X? Something like "Key" perhaps? Because it reminds me of l.sort(key=fn).
    – Paul
    Commented Nov 9, 2009 at 16:24
  • You could almost call it "Equals", so the line reads l.index(Equals(is_odd))
    – tgray
    Commented Nov 9, 2009 at 16:34
  • 5
    I think that what Alex (implicitly) suggested, Satisfies, is a good name for it. Commented Nov 9, 2009 at 21:43
  • @Robert, I do like Satisfies! Commented Nov 10, 2009 at 6:00
  • Sorry to be dense, but how do I express and use Satisfies in a generator that produces all the odd elements of list ref? (Haven't gotten the hang of generators yet, I guess ...) ref = [8,10,4,5,7] def is_odd(x): return x % 2 != 0 class Satisfies(object): def __init__(self, pred): self.pred = pred def __eq__(self, test_this): return self.pred(test_this) print ref.index( Satisfies(is_odd)) #>>>3 Commented Nov 11, 2009 at 5:22
6

Not one single function, but you can do it pretty easily in Python 2:

>>> test = lambda c: c == 'x'
>>> data = ['a', 'b', 'c', 'x', 'y', 'z', 'x']
>>> map(test, data).index(True)
3

If you don't want to evaluate the entire list at once you can use itertools, but it's not as pretty:

>>> from itertools import imap, ifilter
>>> from operator import itemgetter
>>> ifilter(itemgetter(1), enumerate(imap(test, data))).next()[0]
3

Just using a generator expression is probably more readable than itertools though.

Note in Python3, map and filter return lazy iterators and you can just use:

>>> from operator import itemgetter
>>> next(filter(itemgetter(1), enumerate(map(test, data))))[0]
3
5
  • 3
    Unfortunately, this evaluates the entire list - would be nice to have a solution that short-circuits, that is, returns immediately when finding the first match.
    – PaulMcG
    Commented Nov 9, 2009 at 14:25
  • 1
    I think your map().index(True) will not work since this isn't a list but a map object
    – astrochun
    Commented Sep 15, 2022 at 17:33
  • Indeed, you have to convert the map object into a list in order to use index: list(map(test, data)).index(True) Commented Oct 31, 2022 at 10:45
  • 2
    @astrochun In Python 2, map() returned a list. I've edited the answer to clarify that.
    – wjandrea
    Commented Aug 3, 2023 at 20:30
  • Fair point, but Python 2 has been deprecated. I know the original question originated so long ago
    – astrochun
    Commented Aug 3, 2023 at 21:37
3

A variation on Alex's answer. This avoids having to type X every time you want to use is_odd or whichever predicate

>>> class X(object):
...     def __init__(self, pred): self.pred = pred
...     def __eq__(self, other): return self.pred(other)
... 
>>> L = [8,10,4,5,7]
>>> is_odd = X(lambda x: x%2 != 0)
>>> L.index(is_odd)
3
>>> less_than_six = X(lambda x: x<6)
>>> L.index(less_than_six)
2
3

Intuitive one-liner solution:

i = list(map(lambda value: value > 0, data)).index(True)

Explanation:

  1. we use map function to create an iterator containing True or False based on if each element in our list pass the condition in the lambda or not.
  2. then we convert the map output to list
  3. then using the index function, we get the index of the first true which is the same index of the first value passing the condition.
1
  • 2
    This is by far the nicest looking approach, but it doesn't early stop once the condition is satisfied
    – mel
    Commented Aug 21, 2023 at 6:09
0

Using some of my favorite tools that deserve more love (indexOf, compress, count):

from operator import indexOf

def list_func_index(lst, func):
    return indexOf(map(func, lst), True)
from itertools import compress, count

def list_func_index(lst, func):
    return next(compress(count(), map(func, lst)))

Or with your ValueError

from itertools import compress, count

def list_func_index(lst, func):
    for index in compress(count(), map(func, lst)):
        return index
    raise ValueError('no element making func True')
ist_func_index(l, is_odd))

Attempt This Online!

0

Here's a python3 answer, intended to be in the spirit of any() and all():

def index(iterable, desired_item=True):
  for i,item in enumerate(iterable):
    if item == desired_item:
      return i
  raise ValueError(f"no {desired_item} in iterable")

>>> any(n!=0 and n%6==0 and n%15==0 for n in range(10**100))
True
>>> all(n!=0 and n%6==0 and n%15==0 for n in range(10**100))
False
>>> index(n!=0 and n%6==0 and n%15==0 for n in range(10**100))
30

Using the OP's example:

>>> l = [8,10,4,5,7]
>>> def is_odd(x): return x % 2 != 0

>>> any(is_odd(x) for x in l)
True
>>> all(is_odd(x) for x in l)
False
>>> index(is_odd(x) for x in l)
3

Note: I added the optional desired_item argument, despite my general distaste for default arguments, just to ease my guilt at using the name index; this makes it so that it will work for people who assume the function can be used like the following (because of their familiarity with other functions called index in lots of languages, including list's member function in python):

>>> index((2*n for n in range(googol)), 20)
10
-2

you could do this with a list-comprehension:

l = [8,10,4,5,7]
filterl = [a for a in l if a % 2 != 0]

Then filterl will return all members of the list fulfilling the expression a % 2 != 0. I would say a more elegant method...

4
  • Can you edit your answer to be more like the OP's function that has a list and function as parameters?
    – quamrana
    Commented Nov 9, 2009 at 14:25
  • 10
    This is wrong. It returns a list of values, not a single index.
    – recursive
    Commented Nov 9, 2009 at 14:29
  • filterl = [a for a in l if is_odd(a)] Commented Nov 9, 2009 at 14:29
  • 1
    I said that you can do this with a list comprehension and also that it returns a list. Just wanted to give a different option, because I'm not sure what the exact problem of bandana was. Commented Nov 9, 2009 at 14:35

Not the answer you're looking for? Browse other questions tagged or ask your own question.