For maximum readability and maintainability, I would prioritize a construct which goes at the top of the function. So either
- your original
if False: yield
construct, but hoisted to the very first line, or
- a separate decorator which adds generator behavior to a non-generator callable.
(That's assuming you didn't just need a callable which did something and then returned an empty iterable/iterator. If so then you could just use a regular function and return ()
/return iter(())
at the end.)
Imagine the reader of your code sees:
def name_fitting_what_the_function_does():
# We need this function to be an empty generator:
if False: yield
# that crucial stuff that this function exists to do
Having this at the top immediately cues in every reader of this function to this detail, which affects the whole function - affects the expectations and interpretations of this function's behavior and usage.
How long is your function body? More than a couple lines? Then as a reader, I will feel righteous fury and condemnation towards the author if I don't get a cue that this function is a generator until the very end, because I will probably have spent significant mental cost weaving a model in my head based on the assumption that this is a regular function - the first yield
in a generator should ideally be immediately visible, when you don't even know to look for it.
Also, in a function longer than a few lines, a construct at the very beginning of the function is more trustworthy - I can trust that anyone who has looked at a function has probably seen its first line every time they looked at it. That means a higher chance that if that line was mistaken or broken, someone would have spotted it. That means I can be less vigilant for the possibility that this whole thing is actually broken but being used in a way that makes the breakage non-obvious.
If you're working with people who are sufficiently fluently familiar with the workings of Python, you could even leave off that comment, because to someone who immediately remembers that yield
is what makes Python turn a function into a generator, it is obvious that this is the effect, and probably the intent since there is no other reason for correct code to have a non-executed yield
.
Alternatively, you could go the decorator route:
@generator_that_yields_nothing
def name_fitting_what_the_function_does():
# that crucial stuff for which this exists
def generator_that_yields_nothing(wrapped):
@functools.wraps(wrapped)
def wrapper_generator():
if False: yield
wrapped()
return wrapper_generator
if False: yield
at the top of the function, so that it is immediately obvious what you're doing. This is actually an advantage it has over thereturn; yield
solution: either way this construct serves as something of an annotation declaring "I want this to be a generator", and we normally put annotations that effect the entire function at the top for good reason - it helps guide the reader. – Heiglyield
in the method. I've founddef abstract_foo_generator(self) -> Generator[Foo]: yield from ()
to be the least jarring for myself, but I don't like it. – Tarry