86

I'm starting to get used to list comprehension in Python but I'm afraid I'm using it somewhat improperly. I've run into a scenario a few times where I'm using list comprehension but immediately taking the first (and only) item from the list that is generated. Here is an example:

actor = [actor for actor in self.actors if actor.name==actorName][0]

(self.actors contains a list of objects and I'm trying to get to the one with a specific (string) name, which is in actorName.)

I'm trying to pull out the object from the list that matches the parameter I'm looking for. Is this method unreasonable? The dangling [0] makes me feel a bit insecure.

hippietrail
  • 15,848
  • 18
  • 99
  • 158
timfreilly
  • 966
  • 1
  • 9
  • 11
  • 2
    Don't be afraid, It's not _that_ terrible :) One of the answers below that stop looping when the match is found are probably a better choice. `next()` is not too bad, but can become ugly if you try to make it work in 80 char lines. If you are looking up a bunch of actors by name it's going to be a better idea to make a `dict` so you can look them up directly. – John La Rooy Aug 10 '11 at 07:05
  • Fortunately this is not something I am doing frequently or on large sets. I'm fairly good with dictionaries, and will keep that technique in mind. – timfreilly Aug 10 '11 at 07:46
  • Possible duplicate of [What is the best way to get the first item from an iterable matching a condition?](http://stackoverflow.com/questions/2361426/what-is-the-best-way-to-get-the-first-item-from-an-iterable-matching-a-condition) – erickrf Mar 15 '17 at 15:08

4 Answers4

141

You could use a generator expression and next instead. This would be more efficient as well, since an intermediate list is not created and iteration can stop once a match has been found:

actor = next(actor for actor in self.actors if actor.name==actorName)

And as senderle points out, another advantage to this approach is that you can specify a default if no match is found:

actor = next((actor for actor in self.actors if actor.name==actorName), None)
Community
  • 1
  • 1
Zach Kelling
  • 52,505
  • 13
  • 109
  • 108
29

If you want to take the first match of potentially many, next(...) is great. But if you expect exactly one, consider writing it defensively:

[actor] = [actor for actor in self.actors if actor.name==actorName]

This always scans to the end, but unlike [0], the destructuring assignment into [actor] throws a ValueError if there are 0 or more than one match. Perhaps even more important then catching bugs, this communicates your assumption to the reader.

If you want a default for 0 matches, but still catch >1 matches:

[actor] = [actor for actor in self.actors if actor.name==actorName] or [default]

P.S. it's also possible to use a generator expression on right side:

[actor] = (actor for actor in self.actors if actor.name==actorName)

which may be a tiny bit more efficient (?). You could also use tuple syntax on the left side — looks more symmetric but the comma is ugly and too easy to miss IMHO:

(actor,) = (actor for actor in self.actors if actor.name==actorName)
actor, = (actor for actor in self.actors if actor.name==actorName)

(anyway list vs tuple syntax on left side is purely cosmetic doesn't affect behavior)

Beni Cherniavsky-Paskin
  • 9,483
  • 2
  • 50
  • 58
  • 1
    You've probably seen this: `[a, b] = [b, a]` for swapping, though it's usually shown without brackets. It's called "destructuring assignment", or "unpacking". The brackets/parents on the left side are optional, but are needed for the single-element case. The right side can be any python expression, in this case a list comprehension. – Beni Cherniavsky-Paskin Aug 11 '11 at 15:27
  • I see. Thanks for the terminology. I've used packing/unpacking in other situations but have never used it with a single variable to force a single result. – timfreilly Aug 11 '11 at 23:58
2

This post has a custom find() function which works quite well, and a commenter there also linked to this method based on generators. Basically, it sounds like there's no single great way to do this — but these solutions aren't bad.

jtbandes
  • 115,675
  • 35
  • 233
  • 266
  • Interesting that there are blog posts on this subject. Thanks for the overview, I had difficulty finding information on this question. – timfreilly Aug 10 '11 at 07:17
1

Personally I'd to this in a proper loop.

actor = None
for actor in self.actors:
    if actor.name == actorName:
        break

It's quite a bit longer, but it does have the advantage that it stops looping as soon as a match is found.

Daniel Roseman
  • 588,541
  • 66
  • 880
  • 895