在Python中,经常使用这种流控制来编写小的函数:
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
…在此版本中,最主要的收获是:
- The second
yield from
statement, which forms the end of the functions’ likeliest codepath, is within anelse
clause and not at the top level of the functions’ code block. - There is this odd use of the
tuple(…)
constructor toyield 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?
是否存在更好的方法来进行这样的流量控制?他们中的任何一个(或我的任何示例)是否都充满了麻烦的时间复杂性?
†–(即,很难对它们进行窥视孔优化;我不知道–编译器设计比我的薪水高一些)
‡ – 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