Search code examples
pythonpython-3.xgeneratoryield

Problems with yield in Python | Using helper functions


Given a string with many words, I want to reverse the order of the words. If the input is Thanks for all the fish the output should be fish the all for Thanks.

I'm trying to solve this using generators, to avoid creating a new list. I came up with the following solution:

from itertools import islice

def foo(s, begin, end):
    for elem in islice(s, begin, end):
        yield elem

def baz(s):
    i = len(s)
    j = len(s)

    while True:
        i -= 1
        if i == -1:
            foo(s, i+1, j)
            break
        if s[i] == ' ':
            foo(s, i+1, j)
            yield ' '
            j = i


s = "Thanks for all the fish"
g = baz(s)
for x in g: print(x, end='')

However, the output of this code is " " (a string with only the spaces).

On the other hand, if I directly print the elements instead of yielding them, it works:

from itertools import islice

def foo(s, begin, end):
    for elem in islice(s, begin, end):
        print(elem, end='')

def baz(s):
    i = len(s)
    j = len(s)

    while True:
        i -= 1
        if i == -1:
            foo(s, i+1, j)
            break
        if s[i] == ' ':
            foo(s, i+1, j)
            print(' ', end='')
            j = i


s = "Thanks for all the fish"
g = baz(s)

This works and have as output fish the all for Thanks. But I do not want to just be able to print it, I want to have a correct generator.

Finally, I discovered that if avoid the calls to foo it also works:

from itertools import islice

def baz(s):
    i = len(s)
    j = len(s)

    while True:
        i -= 1
        if i == -1:
            for elem in islice(s, i+1, j):
                yield elem
            break
        if s[i] == ' ':
            for elem in islice(s, i+1, j):
                yield elem
            yield ' '
            j = i


s = "Thanks for all the fish"
g = baz(s)
for x in g: print(x, end='')

The output of this is fish the all for Thanks. This version gives me what I want (a correct generator), but I'm repeating code inside its function.

I red the stack exchange thread about what yield does, but I might have understood it wrong. What I understood is that a generator will run until it finds the next yield statement (or the function ends). This problem makes me think this is not exactly how it works, but apart from that I'm clueless.

How can I solve this using helper functions like in the first code snippet?


Solution

  • You can use yield from foo(...) (see also PEP-380 on "Syntax for Delegating to a Subgenerator") or for whatever in foo(...): yield whatever to successivly yield the elements from a generator:

    def baz(s):
        i = len(s)
        j = len(s)
    
        while True:
            i -= 1
            if i == -1:
                yield from foo(s, i+1, j)    # yield from syntax
                break
            if s[i] == ' ':
                for item in foo(s, i+1, j):  # for-loop over generator
                    yield item
                yield ' '
                j = i
    

    But, as @trentcl noted in the comments, in your case you don't need the foo-helper function because it basically does what islice does too. So you could simply replace foo with islice:

    def baz(s):
        i = len(s)
        j = len(s)
    
        while True:
            i -= 1
            if i == -1:
                yield from islice(s, i+1, j)    # yield from syntax
                break
            if s[i] == ' ':
                for item in islice(s, i+1, j):  # for-loop over generator
                    yield item
                yield ' '
                j = i