Alternative to list comprehension if there will be only one result

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.


Solution 1:

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)

Solution 2:

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)

Solution 3:

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.

Solution 4:

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.