How to print graph call as tree?

480 Views Asked by At

For instanse, I have the following code snippet:

def func1(num):
    print(num)

def func2(num):
    func1(num)

def func3(num):
    func2(num)
    func1(num)

def begin():
    pass

def print_graph():
    pass


def main():
    begin()
    func3(3)
    print_graph()

Is there any simple way to print something like that:

func3(1)
    func2(1)
        func1(1)
    func1(1)

I believe, that I have to use globals(), but I don't know, what I do next. It is some sort of study task, therefore I cant use any libraries.

3

There are 3 best solutions below

3
On BEST ANSWER

How about using decorators to print a function's name when it is called? Something like this:

from functools import wraps

def print_on_entry(fn):
    @wraps(fn)
    def wrapper(*args):
        print "{}({})".format(fn.func_name, ", ".join(str(a) for a in args))
        fn(*args)
    return wrapper

Then you can wrap each of your functions up:

func1 = print_on_entry(func1)
func2 = print_on_entry(func2)
func3 = print_on_entry(func3)

So that:

>>> func3(1)
func3(1)
func2(1)
func1(1)
1
func1(1)
1

Of course there are a lot of assumptions in the above code -- the arguments can be converted to strings, for example -- but you get the picture.

0
On

I can go one better than @jme. Here's a version of his decorator that indents and dedents according to your location in the call stack:

import functools

# a factory for decorators
def create_tracer(tab_width):
    indentation_level = 0
    def decorator(f):  # a decorator is a function which takes a function and returns a function
        @functools.wraps(f)
        def wrapper(*args):  # we wish to extend the function that was passed to the decorator, so we define a wrapper function to return
            nonlocal indentation_level  # python 3 only, sorry
            msg = " " * indentation_level + "{}({})".format(f.__name__, ", ".join([str(a) for a in args]))
            print(msg)
            indentation_level += tab_width  # mutate the closure so the next function that is called gets a deeper indentation level
            result = f(*args)
            indentation_level -= tab_width
            return result
        return wrapper
    return decorator

tracer = create_tracer(4)  # create the decorator itself

@tracer
def f1():
    x = f2(5)
    return f3(x)

@tracer
def f2(x):
    return f3(2)*x

@tracer
def f3(x):
    return 4*x

f1()

Output:

f1()
    f2(5)
        f3(2)
    f3(40)

The nonlocal statement allows us to mutate the indentation_level in the outer scope. Upon entering a function, we increase the indentation level so that the next print gets indented further. Then upon exiting we decrease it again.


This is called decorator syntax. It's purely 'syntactic sugar'; the transformation into equivalent code without @ is very simple.

@d
def f():
    pass

is just the same as:

def f():
    pass
f = d(f)

As you can see, @ simply uses the decorator to process the decorated function in some way, and replaces the original function with the result, just like in @jme's answer. It's like Invasion of the Body Snatchers; we are replacing f with something that looks similar to f but behaves differently.


If you're stuck on Python 2, you can simulate the nonlocal statement by using a class with an instance variable. This might make a bit more sense to you, if you've never used decorators before.

# a class which acts like a decorator
class Tracer(object):
    def __init__(self, tab_width):
        self.tab_width = tab_width
        self.indentation_level = 0

    # make the class act like a function (which takes a function and returns a function)
    def __call__(self, f):
        @functools.wraps(f)
        def wrapper(*args):
            msg = " " * self.indentation_level + "{}({})".format(f.__name__, ", ".join([str(a) for a in args]))
            print msg
            self.indentation_level += self.tab_width
            result = f(*args)
            self.indentation_level -= self.tab_width
            return result
        return wrapper

tracer = Tracer(4)

@tracer
def f1():
# etc, as above

You mentioned that you're not allowed to change the existing functions. You can retro-fit the decorator by messing around with globals() (though this generally isn't a good idea unless you really need to do it):

for name, val in globals().items():  # use iteritems() in Python 2
    if name.contains('f'):  # look for the functions we wish to trace
        wrapped_func = tracer(val)
        globals()[name] = wrapped_func  # overwrite the function with our wrapped version

If you don't have access to the source of the module in question, you can achieve something very similar by inspecting the imported module and mutating the items it exports.

The sky's the limit with this approach. You could build this into an industrial-strength code analysis tool by storing the calls in some sort of graph data structure, instead of simply indenting and printing. You could then query your data to answer questions like "which functions in this module are called the most?" or "which functions are the slowest?". In fact, that's a great idea for a library...

0
On

If you don't want to use modify code, you can always use sys.settrace. Here is a simple sample:

import sys
import inspect


class Tracer(object):
    def __init__(self):
        self._indentation_level = 0

    @property
    def indentation_level(self):
        return self._indentation_level

    @indentation_level.setter
    def indentation_level(self, value):
        self._indentation_level = max(0, value)

    def __enter__(self):
        sys.settrace(self)

    def __exit__(self, exc_type, exc_value, traceback):
        sys.settrace(None)

    def __call__(self, frame, event, args):
        frameinfo = inspect.getframeinfo(frame)
        filename = frameinfo.filename

        # Use `in` instead of comparing because you need to cover for `.pyc` files as well.
        if filename in __file__:
            return None

        if event == 'return':
            self.indentation_level -= 1
        elif event == 'call':
            print "{}{}{}".format("  " * self.indentation_level,
                                  frameinfo.function,
                                  inspect.formatargvalues(*inspect.getargvalues(frame)))
            self.indentation_level += 1
        else:
            return None

        return self

Usage:

from tracer import Tracer


def func1(num):
    pass

def func2(num):
    func1(num)

def func3(num):
    func2(num)
    func1(num)


def main():
    with Tracer():
        func3(1)

And results:

func3(num=1)
  func2(num=1)
    func1(num=1)
  func1(num=1)