Alternative to list comprehension if there will be only one result

Question:

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.

Asked By: timfreilly

||

Answers:

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)
Answered By: zeekay

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.

Answered By: Daniel Roseman

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.

Answered By: jtbandes

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)

Categories: questions Tags: ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.