Visualizing nested function calls in python

Burnt Umber

I need a way to visualize nested function calls in python, preferably in a tree-like structure. So, if I have a string that contains f(g(x,h(y))), I'd like to create a tree that makes the levels more readable. For example:

     f()
      |
     g()
     / \
    x  h()
        |
        y

Or, of course, even better, a tree plot like the one that sklearn.tree.plot_tree creates.

This seems like a problem that someone has probably solved long ago, but it has so far resisted my attempts to find it. FYI, this is for the visualization of genetic programming output that tends to have very complex strings like this.

thanks!

update: toytree and toyplot get pretty close, but just not quite there: enter image description here

This is generated with:

 import toytree, toyplot
 mystyle = {"layout": 'down','node_labels':True}
 s = '((x,(y)));'
 toytree.tree(s).draw(**mystyle);

It's close, but the node labels aren't strings...

Update 2: I found another potential solution that gets me closer in text form: https://rosettacode.org/wiki/Visualize_a_tree#Python

tree2 = Node('f')([ 
            Node('g')([
                Node('x')([]),
                Node('h')([
                    Node('y')([])
                ])
            ])
        ])
print('\n\n'.join([drawTree2(True)(False)(tree2)]))

This results in the following:

enter image description here

That's right, but I had to hand convert my string to the Node notation the drawTree2 function needs.

chash

Here's a solution using pyparsing and asciitree. This can be adapted to parse just about anything and to generate whatever data structure is required for plotting. In this case, the code generates nested dictionaries suitable for input to asciitree.

#!/usr/bin/env python3

from collections import OrderedDict

from asciitree import LeftAligned
from pyparsing import Suppress, Word, alphas, Forward, delimitedList, ParseException, Optional


def grammar():
    lpar = Suppress('(')
    rpar = Suppress(')')

    identifier = Word(alphas).setParseAction(lambda t: (t[0], {}))
    function_name = Word(alphas)
    expr = Forward()
    function_arg = delimitedList(expr)
    function = (function_name + lpar + Optional(function_arg) + rpar).setParseAction(lambda t: (t[0] + '()', OrderedDict(t[1:])))
    expr << (function | identifier)

    return function


def parse(expr):
    g = grammar()

    try:
        parsed = g.parseString(expr, parseAll=True)
    except ParseException as e:
        print()
        print(expr)
        print(' ' * e.loc + '^')
        print(e.msg)
        raise

    return dict([parsed[0]])


if __name__ == '__main__':
    expr = 'f(g(x,h(y)))'
    tree = parse(expr)
    print(LeftAligned()(tree))

Output:

f()
 +-- g()
     +-- x
     +-- h()
         +-- y

Edit

With some tweaks, you can build an edge list suitable for plotting in your favorite graph library (igraph example below).

#!/usr/bin/env python3

import igraph

from pyparsing import Suppress, Word, alphas, Forward, delimitedList, ParseException, Optional


class GraphBuilder(object):
    def __init__(self):
        self.labels = {}
        self.edges = []

    def add_edges(self, source, targets):
        for target in targets:
            self.add_edge(source, target)

        return source

    def add_edge(self, source, target):
        x = self.labels.setdefault(source, len(self.labels))
        y = self.labels.setdefault(target, len(self.labels))
        self.edges.append((x, y))

    def build(self):
        g = igraph.Graph()
        g.add_vertices(len(self.labels))
        g.vs['label'] = sorted(self.labels.keys(), key=lambda l: self.labels[l])
        g.add_edges(self.edges)

        return g


def grammar(gb):
    lpar = Suppress('(')
    rpar = Suppress(')')

    identifier = Word(alphas)
    function_name = Word(alphas).setParseAction(lambda t: t[0] + '()')
    expr = Forward()
    function_arg = delimitedList(expr)
    function = (function_name + lpar + Optional(function_arg) + rpar).setParseAction(lambda t: gb.add_edges(t[0], t[1:]))
    expr << (function | identifier)

    return function


def parse(expr, gb):
    g = grammar(gb)
    g.parseString(expr, parseAll=True)


if __name__ == '__main__':
    expr = 'f(g(x,h(y)))'

    gb = GraphBuilder()
    parse(expr, gb)

    g = gb.build()
    layout = g.layout('tree', root=len(gb.labels)-1)
    igraph.plot(g, layout=layout, vertex_size=30, vertex_color='white')

Output plot

Collected from the Internet

Please contact [email protected] to delete if infringement.

edited at
0

Comments

0 comments
Login to comment

Related