9

Is there a built-in python equivalent of std::find_if to find the first element of a list for which a given condition is true? In other words, something like the index() function of a list, but with an arbitrary unary predicate rather than just a test for equality.

I don't want to use list comprehension, because the specific predicate I have in mind is somewhat expensive to compute.

taras
  • 6,566
  • 10
  • 39
  • 50
Simon Segert
  • 421
  • 1
  • 7
  • 23
  • Are you looking for the 'in' command?: if x in list: do something. – Rob Jul 18 '18 at 21:01
  • Did you read this answer? https://stackoverflow.com/a/9542768/10077 Look at "Finding the first occurrence". – Fred Larson Jul 18 '18 at 21:02
  • Seems like you want [assignment expressions](https://www.python.org/dev/peps/pep-0572/)... *"it allows us to conveniently capture a "witness" for an `any()` expression"*. – jonrsharpe Jul 18 '18 at 21:05

2 Answers2

11

Using a tip from an answer to a related question, and borrowing from the answer taras posted, I came up with this:

>>> lst=[1,2,10,3,5,3,4]
>>> next(n for n in lst if n%5==0)
10

A slight modification will give you the index rather than the value:

>>> next(idx for idx,n in enumerate(lst) if n%5==0)
2

Now, if there was no match this will raise an exception StopIteration. You might want use a function that handles the exception and returns None if there was no match:

def first_match(iterable, predicate):
    try:
        return next(idx for idx,n in enumerate(iterable) if predicate(n))
    except StopIteration:
        return None

lst=[1,2,10,3,5,3,4]
print(first_match(lst, lambda x: x%5 == 0))

Note that this uses a generator expression, not a list comprehension. A list comprehension would apply the condition to every member of the list and produce a list of all matches. This applies it to each member until it finds a match and then stops, which is the minimum work to solve the problem.

Fred Larson
  • 60,987
  • 18
  • 112
  • 174
  • Hey! I found the problem of using next() in a lambda interesting, so I looked around a bit for a solution akin to the dict. get()'s default value argument, which substitutes the exception with a default return. I don't know whether this was already the case when you posted, but next() actually works the same. So you could even drop the separate error handling function and reduce your solution to next((n for n in lst if n%5==0), None) – Larry Nov 16 '22 at 15:00
1

Say, you have some predicate function pred and a list lst. You can use itertools.dropwhile to get the first element in lst, for which pred returns True with

itertools.dropwhile(lambda x: not pred(x), lst).next()

It skips all elements for which pred(x) is False and .next() yields you the value for which pred(x) is True.

Edit:

A sample use to find the first element in lst divisible by 5

>>> import itertools
>>> lst = [1,2,10,3,5,3,4]
>>> pred = lambda x: x % 5 == 0 
>>> itertools.dropwhile(lambda x: not pred(x), lst).next()
10
taras
  • 6,566
  • 10
  • 39
  • 50
  • Using the answer I linked in comments above, I was also able to accomplish this with just `next(n for n in lst if n%5==0)`. – Fred Larson Jul 18 '18 at 21:15
  • Yes, definitely, but OP wishes not to use a list comprehension. – taras Jul 18 '18 at 21:17
  • 1
    There's no list comprehension there. Only a generator expression. – Fred Larson Jul 18 '18 at 21:18
  • Well, I am completely fine with your approach, yet they do share the same syntax construct. – taras Jul 18 '18 at 21:21
  • Thank you for this good answer; I accepted the other one because it is "pure" python and doesn't require any imports. – Simon Segert Jul 19 '18 at 13:13
  • @MikeHawk, I find the accepted answer more plausible myself. In fact my approach does the same "under the hood" (if one updated it to return the index instead of the value) – taras Jul 19 '18 at 13:15