0

In Python, one frequently writes small functions with this kind of flow control:

def get_iterstuff(source: Thingy, descr: Optional[str]) -> Iterable[Stuff]:
    """ Return an iterator of Stuff instances from a given Thingy """
    key = prepare_descr(descr)        # may be a “str”, may be None
    val = getattr(source, key, None)  # val has type Optional[Iterable[Stuff]]
    if not val:                       # None, or (in some cases) an empty
        return IterStuff.EMPTY        # sentinel, with the same type as val
    return val                        # the likeliest codepath

… where one if statement can return early – in some such functions, earlier than any of the preparatory function calls, like those in the first two lines of this example here.

This works fine. I mean, I believe there are some peephole-optimization-related complications that arise in functions with multiple return statements. But generally though, like so many other Pythoneers I do this kind of thing literally all the time, to produce testable, maintainable, and legibly flow-controlled functions that can be run without incurring any great big-O wrath.

But so, my question is regarding the manner in which this logic takes a slightly different form, e.g. when the function is a generator that makes use of one or more yield from statements:

def iterstuff(source: Thingy, descr: Optional[str]) -> YieldFrom[Stuff]: # ‡
    key = prepare_descr(descr)          # may be a “str”, may be None
    val = getattr(source, key, None)    # val has type Optional[Iterable[Stuff]]
    if not val:
        yield from tuple()              # … or suchlike
    else:
        yield from val                  # the likeliest codepath

… in this version, the immediately big takeaways are:

  1. The second yield from statement, which forms the end of the functions’ likeliest codepath, is within an else clause and not at the top level of the functions’ code block.
  2. There is this odd use of the tuple(…) constructor to yield from something empty and iterable.

… Now, so, I do know that the use of the else is necessitated by the fact that control falls through yield and yield from statements after they’ve been exhausted (which is a quality that, in other cases, I love to use and abuse). And sticking the tuple(…) thing in there is way easier than, like, all the legwork that’d have to go into concocting the kind of sentinel that IterStuff.EMPTY, from the first function, would be.

But that yield from example does look jankier than its return-based counterpart. More fragile. Less considered. Code-smelly, if you will.

So I ask: what’s the most legible, least consequential, and optimally most Pythonic way to structure the yield from version of this?

Is that tuple() code-wart OK, or are there less programmatically odiferous alternatives?

Do better ways exist for doing flow control like this? Are any of them (or any of my examples) burdened with problematic time complexities?


† – (namely that they’re hard to peephole-optimize; I wouldn’t know – compiler design is a ways above my paygrade)

‡ – the “YieldFrom” generic type alias simplifies annotating these generator functions – as typing.Generator is a bit over-the-top, as written. It looks like this:

class YieldFrom(Generator[T_co, None, None]):
    """ Simple “typing.Generator” alias. The generic Generator
        from “typing” requires three type params:

            • “yield_type” (covariant),
            • “send_type” (contravariant), and
            • “return_type” (covariant).

        … a function containing 1..n “yield” or “yield from”
        statements – without returning anything and unburdened
        by any expectations of calls to its “send(…)” method
        showing up in any type-festooned code – can make use
        of this “YieldFrom[T]” alias. ÷Explict beats implict÷
    """
    pass
fish2000
  • 4,289
  • 2
  • 37
  • 76
  • 1
    `yield from tuple()` is utterly pointless. Doing nothing at all in that code path would be exactly equivalent. – jasonharper Apr 08 '20 at 20:43
  • That is good to know – so yeah if that line was just a `pass`, that codepath would still result in an empty generator returning to the caller? – fish2000 Apr 08 '20 at 21:31
  • 1
    The presence of a `yield` anywhere in the function makes it a generator. There's no requirement that it actually be executed on any given call to the function. – jasonharper Apr 09 '20 at 04:53
  • @jasonharper if you turn your comments into an answer, I’ll totally accept it, as this totally answered my question – fish2000 Jun 03 '20 at 23:36

0 Answers0