How to write Python generator function that never yields anything
Asked Answered
C

3

41

I want to write a Python generator function that never actually yields anything. Basically it's a "do-nothing" drop-in that can be used by other code which expects to call a generator (but doesn't always need results from it). So far I have this:

def empty_generator():
    # ... do some stuff, but don't yield anything
    if False:
        yield

Now, this works OK, but I'm wondering if there's a more expressive way to say the same thing, that is, declare a function to be a generator even if it never yields any value. The trick I've employed above is to show Python a yield statement inside my function, even though it is unreachable.

Civilian answered 7/6, 2011 at 14:15 Comment(6)
Out of curiosity, why does it have to be a generator? I can't imagine what sort of correct caller code could explicitly require a generator...Weisshorn
@Weisshorn I've just discovered that fake generators work great for lazy loading purposes, depending on the context.Sachs
@Ekevoo could you link to an example?Weisshorn
@Weisshorn Here. I'm sure there are better ways to do what I did, but it got the job done. github.com/ekevoo/hfbr/blob/…Sachs
For the record, I would put the 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 the return; 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.Heigl
A good example of why you might need this is a stub method in an abstract class. You want to type it as a generator because that's what you want subclasses to implement, but there's no default logic that you can actually use to put a yield in the method. I've found def abstract_foo_generator(self) -> Generator[Foo]: yield from () to be the least jarring for myself, but I don't like it.Tarry
H
5

For maximum readability and maintainability, I would prioritize a construct which goes at the top of the function. So either

  1. your original if False: yield construct, but hoisted to the very first line, or
  2. 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
Heigl answered 2/8, 2021 at 22:35 Comment(1)
A decade has passed but I appreciate this thoughtful answer!Civilian
C
46

An even shorter solution:

def empty_generator():
  yield from []
Colver answered 26/4, 2016 at 11:44 Comment(3)
Interresting. For Python 3 though.Madrepore
Most elegant and syntax checker friendly in my opinionPillsbury
Suggest using tuple literal () instead of list literal [] if you want to help Python avoid the overhead of empty list creation (depending on how "smart" the Python implementation is, it might conceivably be able to recognize that you never use the list, and optimize accordingly to the point that it's equivalent, but () is just guaranteed immutable by the language, and it is normal and expected that [] is not [] but () is (), and so it takes substantially less cleverness and effort for a Python implementation to make () effectively free versus getting [] to be anywhere near free).Heigl
P
45

Another way is

def empty_generator():
    return
    yield

Not really "more expressive", but shorter. :)

Note that iter([]) or simply [] will do as well.

Pigpen answered 7/6, 2011 at 14:17 Comment(6)
My vote goes to iter([]). [] is not really an iterator (you cannot call .next() on it etc.)Outpouring
iter([]) won't work in my case because I actually do a little work at the top of the function (not shown in my original post, sorry about that). I like the return-then-yield solution all right.Civilian
@John: Be sure to add a comment explaining what it's doing! :)Pigpen
That's strange, because normally return an yield can't be together in a function definition code. But I noticed that if return is replaced with return 25, a syntax error is raised: 'return' with argument inside generator . So there is a difference between return with and return without argument. What is this difference in fact ?Wreath
@eyquem: return in a generator is the same as flow reaching the end of the code -- it will make the generator drop it's frame and raise StopIteration. It doesn't make sense to return a value, just as flow reaching the end of code does not return a value.Pigpen
Note that it's now valid: PEP 380.Reporter
H
5

For maximum readability and maintainability, I would prioritize a construct which goes at the top of the function. So either

  1. your original if False: yield construct, but hoisted to the very first line, or
  2. 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
Heigl answered 2/8, 2021 at 22:35 Comment(1)
A decade has passed but I appreciate this thoughtful answer!Civilian

© 2022 - 2024 — McMap. All rights reserved.