77

I am using generators to perform searches in lists like this simple example:

>>> a = [1,2,3,4]
>>> (i for i, v in enumerate(a) if v == 4).next()
3

(Just to frame the example a bit, I am using very much longer lists compared to the one above, and the entries are a little bit more complicated than int. I do it this way so the entire lists won't be traversed each time I search them)

Now if I would instead change that to i == 666, it would return a StopIteration because it can't find any 666 entry in a.

How can I make it return None instead? I could of course wrap it in a try ... except clause, but is there a more pythonic way to do it?

twasbrillig
  • 17,084
  • 9
  • 43
  • 67
c00kiemonster
  • 22,241
  • 34
  • 95
  • 133
  • Can I ask why you're using generators to search for things? – Conrad.Dean Aug 18 '11 at 03:45
  • What do you expect to happen if you search for something you already passed over? Why not just use the more 'pythonic' way like `if i in a: ...`? – Manny D Aug 18 '11 at 03:46
  • @Manny D, `if i in a` doesn't help if you want to get the index of the found item. – senderle Aug 18 '11 at 04:04
  • @senderle You could use `a.index(i)`. You don't get the nicety of using `enumerate`, true, but I'm really getting at why you'd use a generator to search a list. – Manny D Aug 18 '11 at 04:11
  • 1
    @Manny D, true, but only for iterables with a defined `index` method. Additionally, if you want to test for something other than simple equality -- say if you want to find the first item that's > 5 -- then `index` doesn't help. Still, you're right that in the specific example c00kiemonster gave, `index` is the more sensible approach. – senderle Aug 18 '11 at 04:15
  • Like I said in a comment below, the actual list items are objects and the comparisons involve attributes so I don't really know how to shoehorn them into the `if i in a` idiom – c00kiemonster Aug 18 '11 at 04:29
  • @senderle Hm, that's a good example, didn't think of that. A generator would seem better in that case. @c00kiemonster: I believe that you can define the `__iter__` property of your class to be able to use the `if i in a` idiom. – Manny D Aug 18 '11 at 04:40
  • @Manny D, not a bad idea. thanks for the tip – c00kiemonster Aug 18 '11 at 04:41
  • `StopIteration` is not returned here, but raised; and it comes from the `.next` method, not the generator. – Karl Knechtel May 30 '22 at 19:57

2 Answers2

166

If you are using Python 2.6+ you should use the next built-in function, not the next method (which was replaced with __next__ in 3.x). The next built-in takes an optional default argument to return if the iterator is exhausted, instead of raising StopIteration:

next((i for i, v in enumerate(a) if i == 666), None)
Zach Kelling
  • 52,505
  • 13
  • 109
  • 108
  • I didn't know the built-in function had changed. That makes it a whole lot easier for sure – c00kiemonster Aug 18 '11 at 04:07
  • 2
    Thank you for answering. But I'm using `Python 3.6.3` which is working for my project too. – Johnny Apr 28 '18 at 03:51
  • @Johnny: in Python 3.x, the `.next` **method** was replaced. But the `next` **function** is the same as before, and is what should be used regardless of the version. – Karl Knechtel May 30 '22 at 20:08
7

You can chain the generator with (None,):

from itertools import chain
a = [1,2,3,4]
print chain((i for i, v in enumerate(a) if v == 6), (None,)).next()

but I think a.index(2) will not traverse the full list, when 2 is found, the search is finished. you can test this:

>>> timeit.timeit("a.index(0)", "a=range(10)")
0.19335955439601094
>>> timeit.timeit("a.index(99)", "a=range(100)")
2.1938486138533335
HYRY
  • 94,853
  • 25
  • 187
  • 187
  • 1
    The chain thing was very clever, didn't think of that one. Yes `index()` isn't bad, but in my real case I can't really use it as the list entries are objects rather than variables. EDIT: I am comparing object attributes and other fun stuff rather than just looking for the object itself. – c00kiemonster Aug 18 '11 at 03:48
  • 1
    Sorry to say this, but this is an over-complicated solution. The `next` built-in function already offers this functionality in a clean way. @c00kiemonster, I think you should use (and accept) [zeekay's answer](http://stackoverflow.com/questions/7102050/how-can-i-get-a-python-generator-to-return-none-rather-than-stopiteration/7102204#7102204). – senderle Aug 18 '11 at 04:02