17

I implemented graph traversal as a generator function which yields the node being visited.

Sometimes the user needs to tell the traversal function that the edges outgoing from a particular node shouldn't be followed; in order to support that, the traversal checks the value sent back to it (using generator send() method), and if it's True, regards the node as a leaf for traversal purposes.

The problem is that the simplest user loop is kinda long:

# simplified thanks to @tobias_k
# bfs is the traversal generator function
traversal = bfs(g, start_node)
try:
  n = next(traversal)
  while True:
    # process(n) returns True if don't want to follow edges out of n
    n = traversal.send(process(n))
except StopIteration:
    pass

Is there any way to improve this?

I thought something like this should work:

for n in bfs(g, start_node):
  ???.send(process(n))

but I feel I'm missing the knowledge of some python syntax.

2
  • 2
    Well, you could make it much shorter by putting the try/except outside of the loop; this will save you one set of try/except and the if-condition. Commented Apr 25, 2016 at 8:52
  • @tobias_k fixed, thanks. Commented Apr 25, 2016 at 8:56

7 Answers 7

8

I don't see a way to do this in a regular for loop. However, you could create another generator, that iterates another generator, using some "follow-function" to determine whether to follow the current element, thus encapsulating the tricky parts of your code into a separate function.

def checking_generator(generator, follow_function):
    try:
      x = next(generator)
      while True:
        yield x
        x = generator.send(follow_function(x))
    except StopIteration:
        pass

for n in checking_generator(bfs(g, start_node), process):
    print(n)
Sign up to request clarification or add additional context in comments.

This works! I guess the only disadvantage, apart from having to create an extra utility function, is that the hypothetical ???.send() could be used in many places in the loop, forcing the loop to continue. With this approach, the value can only be sent at the very end of the loop. Too bad python lacks the syntax to support such a basic use case.
@max you can send extra values into the generator by keeping a reference to the original one: traversal = bfs(g, start_node) ; for n in checking_generator(traversal,process): ... traversal.send(...) although in this case checking_generator will still process based on the last node that it processed.
@tobias_k when a generating function ends, it raises a StopIteration. So to wrap the entire body of code in a try to suppress a StopIteration and exit the function... which then raises a StopIteration seems a bit silly. :)
You have to handle StopIteration explicitly otherwise RuntimeError may happen if from __future__ import generator_stop is in effect.
@tobias_k it was introduced in Python 3.5. See the link to the pep and the workaround code in my answer
5

I discovered that my question would have had a one-line answer, using the extended "continue" statement proposed in the earlier version of PEP 342:

for n in bfs(g, start_node):
  continue process(n)

However, while PEP 342 was accepted, that particular feature was withdrawn after this June 2005 discussion between Raymond and Guido:

Raymond Hettinger said:

Let me go on record as a strong -1 for "continue EXPR". The for-loop is our most basic construct and is easily understood in its present form. The same can be said for "continue" and "break" which have the added advantage of a near zero learning curve for people migrating from other languages.

Any urge to complicate these basic statements should be seriously scrutinized and held to high standards of clarity, explainability, obviousness, usefulness, and necessity. IMO, it fails most of those tests.

I would not look forward to explaining "continue EXPR" in the tutorial and think it would stand out as an anti-feature.

[...] The correct argument against "continue EXPR" is that there are no use cases yet; if there were a good use case, the explanation would follow easily.

Guido

If python core developers have since changed their mind about the usefulness of extended "continue", perhaps this could be reintroduced into a future PEP. But, given a nearly identical use case as in this question was already discussed in the quoted thread, and wasn't found persuasive, it seems unlikely.

it could be generator.send_wait or something instead of continue generator. this would solve a lot of the issues.
The issues are with not complexifying continue, not against the functionality of sending without next
4

To simplify the client code, you could use an ordinary bsf() generator and check node.isleaf attribute in it:

 for node in bfs(g, start_node):
     node.isleaf = process(node) # don't follow if `process()` returns True

The disadvantage is that node is mutable. Or you have to pass a shared data structure that tracks leaf nodes: leaf[node] = process(node) where leaf dictionary is passed into bfs() earlier.

If you want to use .send() method explicitly; you have to handle StopIteration. See PEP 479 -- Change StopIteration handling inside generators. You could hide it in a helper function:

def traverse(tree_generator, visitor):
    try:
        node = next(tree_generator)
        while True:
             node = tree_generator.send(visitor(node))
    except StopIteration:
        pass

Example:

traverse(bfs(g, start_node), process)

Comments

1

I don't see this as a frequent use case, consider this as the original generator:

def original_gen():
    for x in range(10):
        should_break = yield x
        if should_break:
            break

If the value of should_break is always calculated based on some function call with x then why not just write the generator like this:

def processing_gen(check_f):
    for x in range(10):
        yield x
        should_break = check_f(x)
        if should_break:
            break

However I usually think of the code that processes the generated values as being written inside the loop (otherwise what is the point of having a loop at all?)

What it really seems you want to do is create a generator where calling the __next__ method really implies send(process(LAST_VALUE)) which can be implemented with a class:

class Followup_generator(): #feel free to use a better name
    def __init__(self,generator,following_function):
        self.gen = generator
        self.process_f = following_function
    def __iter__(self):
        return self
    def __next__(self):
        if hasattr(self,"last_value"):
            return self.send(self.process_f(self.last_value))
        else:
            self.last_value = next(self.gen)
            return self.last_value
    def send(self,arg):
        self.last_value = self.gen.send(arg)
        return self.last_value
    def __getattr__(self,attr):
        "forward other lookups to the generator (.throw etc.)"
        return getattr(self.gen, attr) 

# call signature is the exact same as @tobias_k's checking_generator
traversal = Followup_generator(bfs(g, start_node), process)
for n in traversal: 
    print(n)
    n = traversal.send(DATA) #you'd be able to send extra values to it

However this still doesn't see this as frequently used, I'd be perfectly fine with a while loop, although I'd put the .send call at the top:

traversal = bfs(g, start_node)
send_value = None
while True:
    n = traversal.send(send_value)
    #code for loop, ending in calculating the next send_value
    send_value = process(n)

And you might wrap that in a try: ... except StopIteration:pass although I find that simply waiting for an error to raise is better expressed with a context manager:

class Catch:
    def __init__(self,exc_type):
        if issubclass(exc_type,BaseException):
            self.catch_type = exc_type
        else:
            raise TypeError("can only catch Exceptions")
    def __enter__(self):
        return self
    def __exit__(self,exc_type,err, tb):
        if issubclass(exc_type, self.catch_type):
            self.err = err
            return True


with Catch(StopIteration):
    traversal = bfs(g, start_node)
    send_value = None
    while True:
        n = traversal.send(send_value)
        #code for loop, ending in calculating the next send_value
        send_value = process(n)

Comments

1

Probably this is the answer to the question from the thread's topic.

Take a look at the additional empty yields statements inside the traversal function and custom send function, that does the magical job.

# tested with Python 3.7

def traversal(n):
    for i in range(n):
        yield i, '%s[%s] %s' % (' ' * (4 - n), n, i)
        stop = yield
        if stop:
            yield  # here's the first part of the magic
        else:
            yield  # the same as above
            yield from traversal(int(n / 2))


def send(generator, value):
    next(generator)   # here's the second part of the magic
    generator.send(value)


g = traversal(4)

for i, (num, msg) in enumerate(g):
    print('>', i, msg)
    stop = num % 2 == 0
    send(g, stop)

Comments

1

I've written a small class SettableGenerator which uses a method to receive the value to be send and then forwards it to the actual generator when __next__ is invoked.

With this you can write:

gen = SettableGenerator(bfs(g, start_node))
for n in gen:
  gen.set(process(n))

Comments

1

Let's consider the following generator. It generates numbers from 0 to 9. For every generated number, it gets an input and stores it into ret:

def count_to_nine():
    # Output: numbers from 0 to 9
    # Input: converted numbers
    ret = []
    for i in range(10):
        # Yield a number, get something back
        val = (yield i)
        # Remember that "something"
        ret.append(val)
    return ret

You can, indeed, iterate it using next() + send(), but the best way is to iterate using send() alone:

g = count_to_nine()
value = None  # to make sure that the first send() gives a None
while True:
    value = g.send(value)  # send the previously generated value, get a new one
    value = f'#{value}'

Here's the result:

StopIteration: ['#0', '#1', '#2', '#3', '#4', '#5', '#6', '#7', '#8', '#9']

If you want that output, catch the StopIteration and get the result from it.

Cheers!

Comments

Your Answer

Draft saved
Draft discarded

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.