2014-12-18 13 views
2

Для instanse, я следующий фрагмент кода:Как напечатать вызов графика как дерево?

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() 

Есть ли простой способ напечатать что-то вроде этого:

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

Я считаю, что я должен использовать globals(), но дон Не знаю, что я буду делать дальше. Это своего рода учебная задача, поэтому я не могу использовать какие-либо библиотеки.

+0

Не легко. Вероятно, вы могли бы это сделать с помощью встроенных модулей 'ast',' compiler' и 'inspect', используя' inspect.getsource (...) 'для извлечения исходного кода самого файла Python, а затем используя 'compiler.parse (...)', чтобы получить структуру данных [AST] (http://en.wikipedia.org/wiki/Abstract_syntax_tree), которую вы можете исследовать, чтобы выяснить, что называет. –

+0

Присоединение к 'sys.settrace' и проверка объектов фрейма должны быть проще и точнее, чем любой статический анализ (что предполагает предложение Джереми Бэнкса). С другой стороны, для этого потребуется обильное количество черной магии, специфичной для CPython. – delnan

+0

Я считаю, что есть более простой способ, чем статический анализ и встроенный отладчик. Это связано с тем, что моя задача представлена ​​в конкретном случае, а не в общем случае. – Max

ответ

2

Как насчет использования декораторов для печати имени функции при ее вызове? Что-то вроде этого:

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 

Затем вы можете обернуть каждый из ваших функций до:

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

Так что:

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

Конечно, есть много предположений в коде выше - аргументы могут быть преобразованы в строки, например, - но вы получите изображение.

+0

Это решение для работы, но не для меня. Это связано с тем, что я могу изменить только 'print_graph()' и 'begin()'. У меня нет прав на изменение других функций. – Max

+0

@Max 'func1',' func2' и 'func3' не изменяются * так, как будто они * завернуты *. Затем они переназначаются на свои старые имена функций, так что, когда 'func3' смотрит' func2', он найдет нашу завернутую версию. – jme

+0

@Max: вы можете динамически добавлять оболочку с чем-то вроде 'для имени, значения в globals(). Items(): if callable (value): globals() [name] = print_on_entry (value)' –

3

Я могу пойти лучше, чем @jme. Вот версия его декоратора, что отступы и dedents согласно вашему положению в стеке вызовов:

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() 

Выход:

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

nonlocal заявление позволяет мутировать indentation_level во внешней области. При входе в функцию мы увеличиваем уровень отступа, чтобы следующий print получил отступы дальше. Затем, после выхода, мы снова уменьшаем его.


Это называется decorator syntax. Это чисто «синтаксический сахар»; преобразование в эквивалентный код без @ очень прост.

@d 
def f(): 
    pass 

это так же, как:

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

Как вы можете видеть, @ просто использует декоратор для обработки декорированной функции в некотором роде, и заменяет исходную функцию с результатом, так же, как в ответе @ jme. Это как Invasion of the Body Snatchers; мы заменяем f на что-то похожее на f, но ведет себя по-другому.


Если вы застряли на Python 2, вы можете имитировать nonlocal заявление, используя класс с переменной экземпляра. Это может сделать для вас немного больше смысла, если вы никогда раньше не использовали декораторов.

# 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 

Вы сказали, что вы не можете изменить существующие функции.Вы можете ретро-подходят декоратор по возни с globals() (хотя это обычно не является хорошей идеей, если вы действительно необходимости делать это):

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 

Если у вас нет доступа к источнику данного модуля вы можете добиться чего-то очень похожего, проверив импортированный модуль и изменив экспортируемые им элементы.

Небо предел с этим подходом. Вы могли бы построить это в инструменте анализа кода промышленного уровня, сохранив вызовы в некотором виде структуры данных графа, а не просто отступы и печать. Затем вы можете запросить свои данные, чтобы ответить на такие вопросы, как «какие функции в этом модуле называются наиболее?» или «какие функции являются самыми медленными?». На самом деле, это отличная идея для библиотеки ...

3

Если вы не хотите использовать код модификации, вы всегда можете использовать sys.settrace. Вот простой пример:

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 

Использование:

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) 

И результаты:

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