0

Can anyone help me step through the logic of the program shown below? I tried using the Python debugger. This did not help me that much though.

I do not understand the following:

  • preorder_traversal()

    • For instance at the yield (parent, root) line of code; does the function return these values as a generator at this point to the caller or does it return the generator and then keep going inside the preorder_traversal() function?

    • Also, mind completely melts when trying to wrap my head around the recursive call to preorder_traversal(). Does anyone know of a way to understand this? Like a truth table or something like that that I can use to manually step through the program with a pen and paper or notepad or whatever. I think the most complicated part of this is the nesting and the recursion.

  • I do not understand the Node inside a Node inside a Node, etc. Or the whole adding and edge part which adds a Node to a list.

Code

class Node(object):
    """A simple digraph where each node knows about the other nodes
    it leads to.
    """
    def __init__(self, name):
        self.name = name
        self.connections = []
        return

    def add_edge(self, node):
        "Create an edge between this node and the other."
        self.connections.append(node)
        return

    def __iter__(self):
        return iter(self.connections)

def preorder_traversal(root, seen=None, parent=None):
    """Generator function to yield the edges via a preorder traversal."""
    if seen is None:
        seen = set()
    yield (parent, root)
    if root in seen:
        return
    seen.add(root)
    for node in root:
        for (parent, subnode) in preorder_traversal(node, seen, root):
            yield (parent, subnode)
    return

def show_edges(root):
    "Print all of the edges in the graph."
    for parent, child in preorder_traversal(root):
        if not parent:
            continue
        print '%5s -> %2s (%s)' % (parent.name, child.name, id(child))

# Set up the nodes.
root = Node('root')
a = Node('a')
b = Node('b')
c = Node('c')

# Add edges between them.
root.add_edge(a)
root.add_edge(b)
a.add_edge(b)
b.add_edge(a)
b.add_edge(c)
a.add_edge(a)

print 'ORIGINAL GRAPH:'
show_edges(root)

Thank-you for reading this.

ljeabmreosn
  • 810
  • 8
  • 23
user3870315
  • 1,171
  • 3
  • 15
  • 37
  • Possible duplicate of [What does the "yield" keyword do?](https://stackoverflow.com/questions/231767/what-does-the-yield-keyword-do) – ephemient Aug 15 '17 at 17:36
  • What about the recursive algorithm confusion part? – user3870315 Aug 15 '17 at 18:09
  • Possible duplicate of [Having trouble understanding tree traversal recursive functions](https://stackoverflow.com/questions/33818795/having-trouble-understanding-tree-traversal-recursive-functions) – ephemient Aug 15 '17 at 18:47

1 Answers1

1

As for the yield operator, yield allows the function to be a generator, thus lazy. For this particular example, the need for a generator is not needed and its only benefit is much better readability (i.e. for _ in _). Abstractly, yield (parent, root) is returned by using the next() operation on the generator. Then, when next() is called again, the generator continues to dynamically execute the remaining code in the function.

As for the recursive call, this is fairly common when doing any type of graph traversal. Furthermore, a graph is a recursive data structure.

Here is a good resource for understanding graph traversals.

Below is a slightly modified version of the preorder_traversal() (easier to read) which has some comments:

def preorder_traversal(root, seen=set(), parent=None):
    """Generator function to yield the edges via a preorder traversal."""
    yield (parent, root)

    # avoid cycle
    if root not in seen:
        seen.add(root)

        # for each neighbor of the root
        for node in root:

            # for each (parent, neighbor) pair in the subgraph 
            # not containing the nodes already seen 
            for (parent, subnode) in preorder_traversal(node, seen, root):
                yield (parent, subnode)

To demonstrate the lazy nature of a Python generator, consider the custom irange() generator where irange(n) == xrange(n+1):

def irange(limit):
    current_number = 0
    while current_number <= limit:
        yield current_number
        current_number += 1

If you do a = irange(9999999999999999999999), no code in irange() is performed until next() is called on it.

To understand recursion with generators, consider the custom rrange() generator where rrange(n) == reversed(irange(n)):

def rrange(limit):
    if limit >= 0:
        yield limit
        for num in rrange(limit - 1):
            yield num 
ljeabmreosn
  • 810
  • 8
  • 23