2

First, consider the following code (I'm going to discuss several versions of subgen() next):

>>> def maingen(i):
...    print("maingen started")
...    yield from subgen(i)
...    print("maingen finished")
...
>>> for i in maingen(5):
...     print(i)
...     

I want to write several subgen generator functions.

A normal one is:

>>> def subgen_1(i):
...     yield i + 1
...     yield i + 2
...     yield i + 3

No prob, output is as expected:

maingen started
6
7
8
maingen finished

Now, I want an other version of subgen which yields nothing...

If I try that:

>>> def subgen_2(i):
...     i * 3

I've an exception:

maingen started
Traceback (most recent call last):
  ...
TypeError: 'NoneType' object is not iterable

It's expected: subgen_2 isn't a generator function.

OK, next. I can read somewhere (like the first answer here) that I've to raise a StopIteration. (Note that, as a newbie, I can't comment this answer.)

>>> def subgen_3(i):
...     i * 3
...     raise StopIteration()
...     

As identified in PEP 479, "Finally, the proposal also clears up the confusion about how to terminate a generator: the proper way is return, not raise StopIteration.", I only get:

maingen started

(no maingen finished...)

And the only way I've found to get things OK is:

>>> def subgen_4(i):
...     i * 3
...     return
...     yield
... 

With this, I get:

maingen started
maingen finished

Hourrah! But this solution isn't beautiful...

Does anyone have a better or a more pythonic idea?

Is it possible to write a decorator to secretly add the ugly yield statement?

Community
  • 1
  • 1
Julien A
  • 111
  • 9
  • a function could return a generator without necessarily being a generator itself, you could just `return iter([])` if you want the function to run and then result in an empty iterator. – Tadhg McDonald-Jensen May 18 '16 at 15:19
  • if `subgen_2` does not actually `yield` anything why do you use `yield from subgen_2()` in the first place? – Tadhg McDonald-Jensen May 18 '16 at 15:22
  • That answer you linked to only mentioned raising a `StopIteration` to stop the generator (without yielding anything first). It does not change the fact though, that you need to have a `yield` statement in the function body (syntactically), to make it a generator function. – poke May 18 '16 at 15:23
  • 1
    use [this answer](http://stackoverflow.com/a/6395075/5827215) or poke's idea of `yield from []`, in any case you are asking how to make an empty generator which is exactly the same as [yield break in Python](http://stackoverflow.com/questions/6395063/yield-break-in-python). – Tadhg McDonald-Jensen May 18 '16 at 15:38

2 Answers2

2

If you want a sub generator to yield nothing, then return an empty iterator:

def subgen_2(i):
    i * 3
    return iter([]) #empty iterator

The reason you were getting TypeError: 'NoneType' object is not iterable is because without a yield statement in your sub-generator it was not a generator, but a regular function that implicitly returned None.

By returning a valid iterator that doesn't produce any values you will be able to yield from subgen_2() without error and without generating any additional values.

Another way to hide this (I don't really see why you would want to) is to make a decorator that literally just calls your function then does return iter(()) or yield from [].

def gen_nothing(f):
    @functools.wraps(f)
    def wrapper(*args,**kw):
        f(*args,**kw)
        yield from []
    return wrapper

But the only difference this produces is that the stack will require one additional frame for the wrapper, which means your Traceback messages will have a bit more noise:

Traceback (most recent call last):
  File "/Users/Tadhg/Documents/codes/test.py", line 15, in <module>
    for i in subgen():
  File "/Users/Tadhg/Documents/codes/test.py", line 6, in wrapper
    f(*args,**kw)
  File "/Users/Tadhg/Documents/codes/test.py", line 12, in subgen
    3/0
ZeroDivisionError: division by zero
Tadhg McDonald-Jensen
  • 20,699
  • 5
  • 35
  • 59
  • 1
    Since `return iter([])` does not turn the function into a generator itself, and you might prefer that, you can use something like `yield from []` to do the equivalent thing and produce a generator function instead. – poke May 18 '16 at 15:33
  • I was asking for a more beautiful way... The `yield from []` solution is a good one. – Julien A May 18 '16 at 15:46
  • But I was also asking if it's possible to write a decorator to hide that "fake" statement... – Julien A May 18 '16 at 15:46
  • Thanks. I'm not a fan too... And I just posted an answer to show how I prefer to rewrite the `maingen` generator function. – Julien A May 18 '16 at 21:57
0

After reading all comments, I think the more pythonic way is to rewrite the maingen:

def maingen(i):
    print("maingen started")
    gen = subgen(i)
    if gen:
        yield from gen
    print("maingen finished")

With that, a subgen which is really a generator function:

def subgen_1(i):
    yield i + 1
    yield i + 2
    yield i + 3

or a subgen which is a simple function:

def subgen_2(i):
    i * 3

are both "accepted" when "injected" in the maingen generator function, and don't need some ugly yield statements.

Julien A
  • 111
  • 9