16
\$\begingroup\$

I've been trying to figure out what's the best, most Pythonic way to get the first element of a sequence that matches a condition. Here are three options:

  1. I don't like that I've written caller[0].f_locals.get("runner") twice, but it feels most Pythonic otherwise.
    runner = next(
        caller[0].f_locals.get("runner")
        for caller in inspect.stack()
        if isinstance(caller[0].f_locals.get("runner"), AbstractRunner)
    )
    
  2. Is it weird to rely on an assignment during the loop?
    for caller in inspect.stack():
        runner = caller[0].f_locals.get("runner")
        if isinstance(runner, AbstractRunner):
            break
    
  3. The second line is Pythonic and all, but the first line is only there to set up a generator where you no longer have to compute the expression (only apply the conditional in the second line).
    generator = (caller[0].f_locals.get("runner") for caller in inspect.stack())
    runner = next(x for x in generator if isinstance(x, AbstractRunner))
    

There's no performance difference at all from my tests.

\$\endgroup\$
1
  • \$\begingroup\$ You don't have to do the assignment twice if you nest, runner = next(runner for runner in (caller[0].f_locals.get("runner") for caller in inspect.stack()) if isinstance(runner, AbstractRunner)). This is basically a hybrid of your first and third options. I'd consider it fairly pythonic if written out in multiple lines in a readable way. \$\endgroup\$
    – kojiro
    Commented Dec 16, 2020 at 12:58

4 Answers 4

18
\$\begingroup\$

This is a use case for the walrus operator :=, which was introduced in Python 3.8. It allows you to write 1) without repeating yourself:

runner = next(
    r
    for caller in inspect.stack()
    if isinstance(r := caller[0].f_locals.get("runner"), AbstractRunner)
)

Whether or not you find this more readable or if this is too much nestedness in the if part is up to personal taste.

\$\endgroup\$
7
  • \$\begingroup\$ This is great! I unfortunately can't use this, as my code is for a library plugin that needs to support Python 3.6-3.8, but accepting as this answers my question as to how to most elegantly avoid duplication. \$\endgroup\$
    – deepyaman
    Commented Dec 14, 2020 at 2:27
  • 1
    \$\begingroup\$ The walrus operator can be really useful. But in this case, it really has a weird flow. It doesn't feel Pythonic at all to use a variable before it is (apparently) defined. \$\endgroup\$ Commented Dec 15, 2020 at 18:26
  • 1
    \$\begingroup\$ @EricDuminil Isn't that because of the generator expression rather than because of the walrus? \$\endgroup\$ Commented Dec 16, 2020 at 5:54
  • 1
    \$\begingroup\$ @superbrain: It's the mix of both which I find not readable. x for x in some_list if foo(x) can be read almost as an English instance. It's hard to read the above code without "by the way" or "I forgot to mention what r is". OP's 3rd proposal is very readable and the closest to the Zen of Python. We can use the walrus operator here but I don't think we should. My $0.02. \$\endgroup\$ Commented Dec 16, 2020 at 8:35
  • 1
    \$\begingroup\$ @EricDuminil For what it's worth, I kind of agree. I always want to write the SyntaxError [y := f(x) for x in foo if g(y)] instead of the "correct" one [y for x in foo if g(y := f(x))]. \$\endgroup\$
    – Graipher
    Commented Dec 16, 2020 at 8:39
13
\$\begingroup\$

I have to be honest, I actually prefer option 3, and it's what I've used previously. It gives a clear separation of steps: "Produce the data, then filter it".

For a simpler case, I might go with @Graipher's suggestion. I have nothing against :=; I use it regularly. That last line of his in particular though is getting a bit noisy, and takes some processing to see what's going on.

\$\endgroup\$
8
\$\begingroup\$

You could also avoid your duplication with the single-value list idiom:

runner = next(
    runner
    for caller in inspect.stack()
    for runner in [caller[0].f_locals.get("runner")]
    if isinstance(runner, AbstractRunner)
)

Separates getting the runner from checking its type, works in older Python's as well, doesn't leak like the walrus does, and since 3.9, it's "as fast as a simple assignment".

\$\endgroup\$
1
  • 1
    \$\begingroup\$ Interesting. It's easier to read than the := answer, and it has a better flow. I personally still find it less readable than OP's 3rd version, though. \$\endgroup\$ Commented Dec 17, 2020 at 12:02
6
\$\begingroup\$

I would vote against the walrus operator here; it's a clear case of doing two things at once.

Your original (#1) is probably good. I like that you're directly assigning the final value to its variable without preamble. If the repetition is really bugging you (or if the get function is expensive), then you could just nest generators

runner = next(
    r
    for r in (caller[0].f_locals.get("runner")
              for caller in inspect.stack())
    if isinstance(r, AbstractRunner)
)

or abandon comprehension-syntax

runner = next(filter(lambda r: isinstance(r, AbstractRunner),
                     map(lambda c: c[0].f_locals.get("runner"),
                         inspect.stack())))

Some people don't like lambdas; they're one of python's weak points. Unfortunately we hit several other weak points if we try to get rid of them: functools.partial won't work on isinstance (it takes positional args only, and we want to lock the second arg), and there's no built-in composition operator which is what we'd need for the map.

Really any of the repetitious comprehension, the nested comprehension, or the filter-&-map, or some hybrid, would be fine.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.