32

How do I find an object in a sequence satisfying a particular criterion?

List comprehension and filter go through the entire list. Is the only alternative a handmade loop?

mylist = [10, 2, 20, 5, 50]
find(mylist, lambda x:x>10) # Returns 20
martineau
  • 119,623
  • 25
  • 170
  • 301
Salil
  • 9,534
  • 9
  • 42
  • 56

4 Answers4

39

Here's the pattern I use:

mylist = [10, 2, 20, 5, 50]
found = next(i for i in mylist if predicate(i))

Or, in Python 2.4/2.5, next() is a not a builtin:

found = (i for i in mylist if predicate(i)).next()

Do note that next() raises StopIteration if no element was found. In most cases, that's probably good. You asked for the first element, no such element exists, and so the program probably cannot continue.

If, on the other hand, you do know what to do in that case, you can supply a default to next():

conf_files = ['~/.foorc', '/etc/foorc']
conf_file = next((f for f in conf_files if os.path.exists(f)),
                 '/usr/lib/share/foo.defaults')
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
SingleNegationElimination
  • 151,563
  • 33
  • 264
  • 304
  • Have you come out with it yourself or have you seen it somewhere and if so where? Also, wouldn't it be better to use built-in `next` and pass the default value (empty list in this case probably)? – Piotr Dobrogost Oct 02 '13 at 09:56
  • 1
    `next()` was introduced in Python 2.6 already - http://docs.python.org/2.6/library/functions.html?highlight=next#next – Piotr Dobrogost Oct 02 '13 at 19:55
  • @Piotr: I think that may depend on the particular use case. If you want the first element of an empty sequence, there is no correct value to return, it has no first element. It might make perfectly good sense to raise an exception. `bool(foo(next(filter(foo, []), [])) != True`, that is, unless `foo` is something like `lambda x: x == []`, and `[]` is certainly not *in* `[]`, so it's a lie in any case. – SingleNegationElimination Oct 02 '13 at 22:31
  • Your code throws an exception `StopIteration` if generator describes empty sequence. – Alex G.P. Dec 08 '14 at 16:04
14

Actually, in Python 3, at least, filter doesn't go through the entire list.

To double check:

def test_it(x):
    print(x)
    return x>10

var = next(filter(test_it, range(20)))

In Python 3.2, that prints out 0-11, and assigns var to 11.

In 2.x versions of Python you may need to use itertools.ifilter.

Jeremiah
  • 1,437
  • 8
  • 17
  • Good call; `zip`, `map`, and `filter` all become lazy in Python3. (Replacements for Python2's `imap`, `izip`, and `ifilter`.) – mechanical_meat May 18 '11 at 03:26
  • I prefer this to using `for` as it is much clearer. **Note 1:** this will also throw `StopIteration` if no match. **Note 2:** if your list is sorted, consider using [`bisect`](https://docs.python.org/3.0/library/bisect.html) if appropriate. – Jonathan H Apr 02 '18 at 09:52
6

If you only want the first greater than 10 you can use itertools.ifilter:

import itertools
first_gt10 = itertools.ifilter(lambda x: x>10, [10, 2, 20, 5, 50]).next()

If you want all greater than 10, it may be simplest to use a list-comprehension:

all_gt10 = [i for i in mylist if i > 10]
mechanical_meat
  • 163,903
  • 24
  • 228
  • 223
-1

Too lazy to write:

mylist = [10, 2, 20, 5, 50]
max(mylist, key=lambda x: x>10)
razpeitia
  • 1,947
  • 4
  • 16
  • 36