Python程序的追踪表

8 投票
3 回答
9643 浏览
提问于 2025-04-15 15:30

有没有办法获取一个Python程序的追踪表?或者让一个程序运行另一个程序并获取它的追踪表?我是一名老师,想要准确无误地验证我们在测试中使用的追踪问题的答案。

举个例子,假设我有一个名为 problem1.py 的Python程序,内容如下:

problem1.py

 a = 1
 b = 2

 a = a + b

执行这个假设的程序 traceTable.py 应该是这样的:

 $ python traceTable.py problem1.py
 L || a | b
 1 || 1 |
 2 || 1 | 2
 4 || 3 | 2

(或者用不同的语法表示相同的信息)

我查过 trace 模块,但没找到支持这个功能的方法。


更新

各位先生女士:根据Ned Batchelder的优秀建议,我给你们带来了 traceTable.py

嗯……差不多吧。正如你们在Ned Batchelder的例子中看到的,frame.f_lineno 有时候并不是那么直观(例如,第3行和第4行都被算作第4行),但行号足够接近,可以作为一个相当好的参考。而且,所有的计算都是正确的。

我用一个包含 if 语句的长程序测试过,结果得到了正确的表格(除了行号)。

你们还会注意到,我的程序比Ned Batchelder的概念验证程序要长得多,因为我考虑到了他提到的更复杂的数据生态系统。在使用 execfile 以及管理它所需的所有变量和减少噪音(比如 ignored_variables)的范围内,还要产生正确的字符串输出,需要更多的代码:

traceTable.py

 '''
 Usage: python traceTable.py program

     -program  Python program to be traced
 '''

 import sys

 if len(sys.argv) < 2:
      print __doc__
      exit()
 else:
      file_name = sys.argv[1]

 past_locals = {}
 variable_list = []
 table_content = ""

 ignored_variables = set([
      'file_name',
      'trace',
      'sys',
      'past_locals',
      'variable_list',
      'table_content',
      'getattr',
      'name',
      'self',
      'object',
      'consumed',
      'data',
      'ignored_variables'])

 def trace(frame, event, arg_unused):
      global past_locals, variable_list, table_content, ignored_variables
      relevant_locals = {}
      all_locals = frame.f_locals.copy()
      for k,v in all_locals.items():
           if not k.startswith("__") and k not in ignored_variables:
                relevant_locals[k] = v
      if len(relevant_locals) > 0 and past_locals != relevant_locals:
           for i in relevant_locals:
                if i not in past_locals:
                     variable_list.append(i)
           table_content += str(frame.f_lineno) + " || "
           for variable in variable_list:
                table_content += str(relevant_locals[variable]) + " | "
           table_content = table_content[:-2]
           table_content += '\n'
           past_locals = relevant_locals
      return trace

 sys.settrace(trace)

 execfile(file_name)

 table_header = "L || "
 for variable in variable_list:
      table_header += variable + ' | '
 table_header = table_header[:-2]
 print table_header
 print table_content

当调用时,它会产生以下输出:

 $ python traceTable.py problem1.py
 L || a | b
 2 || 1
 4 || 1 | 2
 4 || 3 | 2

3 个回答

0

根据ned-batchelder的建议,作为一名老师,我创建了一个叫做Tracer的类,帮助生成LaTeX格式的longtable,用来展示程序的执行过程,特别是一些特定变量的变化。这个类可以绕过input()函数,方便自动化处理,尤其是在通过强大的bashful LaTeX包中的\bash宏调用时。

tracer.py:

import sys
class Tracer():
    def __init__(self, varList=[], startLine=1, jeuEssai=[]):
        """
        Arguments :
        \tvarList\ttraced variable list (used as column header)
        \tstartLine\toffset numbering line from the beginning of the program
        \tjeuEssai\tinput values to be sent to the automated input bypass
        """
        self.traced_variables = varList
        self.traced_line_start = startLine
        self.input_values = jeuEssai
        self.input_cursor = int(0)
        self.traced_variables_new_values = dict( (k, '') for k in self.traced_variables)

        print("\\begin{longtable}{c*{%i}{>{\\ttfamily}c}}" % len(self.traced_variables), file=sys.stderr, flush=True)
        print("\t\\hline\\no ligne",end='', file=sys.stderr)
        for header in self.traced_variables:
            print(" &", header,end='', file=sys.stderr)
        print(" \\\\ \\hline", file=sys.stderr)
        sys.settrace(self.tracer_programme_latex)


    def tracer_programme_latex(self, frame, event, args):
        if frame.f_code.co_name not in ['input','print','close']:
            if event == "line":
                output = str()
                for var in self.traced_variables:
                    current_val = str(frame.f_locals.get(var, "-"))
                    if str(self.traced_variables_new_values.get(var, "-")) != current_val:
                        self.traced_variables_new_values[var] = current_val
                        current_val = "\hit{}" + current_val
                    output += " & "
                    output += current_val
                output += " \\\\"
                print("\t%s%s" % (str(frame.f_lineno - self.traced_line_start), output), file=sys.stderr, flush=True)
        return self.tracer_programme_latex


    def close(self):
        """Close the 'longtable' LaTeX environnement."""
        print("\\end{longtable}", file=sys.stderr, flush=True)


    def input(self, prompt=None):
        """
        bypass de la fonction 'input()' pour injecter
        les valeurs d'essais.
        Le jeu d'essai est fourni de manière cyclique. Cela peut
        causer des boucles infinies si vous ne fournissez pas une
        valeur permettant de réaliser l'arrêt des entrées (dans le
        cas bien-sûr où 'input()' est appelé dans une boucle).
        """
        self.input_cursor = (1 + self.input_cursor) % len(self.input_values)
        return self.input_values[self.input_cursor - 1]


    def print(self, *args):
        pass

接下来你可以看到一个例子,以及生成的输出:

program.py:

def factor():
    question = "Give a number: "
    number = float(input(question))
    product = 1
    while number != 0 :
        product *= number
        print("Product:", product)
        number = float(input(question))

if __name__ == "__main__":
    import sys
    TRACING = len(sys.argv) == 2 and sys.argv[1] == 'trace'
    if TRACING:
        from tracer import Tracer
        t = Tracer(varList=['question','number','product'], startLine=2, jeuEssai=[7,6,5,-8,0])
        input = t.input

    factor()
    if TRACING:
        t.close()

标准输出: (当通过python3 program.py调用时)

Give a number: 7
Product: 7.0
Give a number: 6
Product: 42.0
Give a number: 5
Product: 210.0
Give a number: -8
Product: -1680.0
Give a number: 0

使用Tracer的输出: (当通过python3 program.py trace 1>/dev/null调用时)

\begin{longtable}{c*{3}{>{\ttfamily}c}}
    \hline\no ligne & question & number & product \\ \hline
    0 & \hit{}- & \hit{}- & \hit{}- \\
    1 & \hit{}Give a number:  & - & - \\
    2 & Give a number:  & \hit{}7.0 & - \\
    3 & Give a number:  & 7.0 & \hit{}1 \\
    4 & Give a number:  & 7.0 & 1 \\
    5 & Give a number:  & 7.0 & \hit{}7.0 \\
    6 & Give a number:  & 7.0 & 7.0 \\
    3 & Give a number:  & \hit{}6.0 & 7.0 \\
    4 & Give a number:  & 6.0 & 7.0 \\
    5 & Give a number:  & 6.0 & \hit{}42.0 \\
    6 & Give a number:  & 6.0 & 42.0 \\
    3 & Give a number:  & \hit{}5.0 & 42.0 \\
    4 & Give a number:  & 5.0 & 42.0 \\
    5 & Give a number:  & 5.0 & \hit{}210.0 \\
    6 & Give a number:  & 5.0 & 210.0 \\
    3 & Give a number:  & \hit{}-8.0 & 210.0 \\
    4 & Give a number:  & -8.0 & 210.0 \\
    5 & Give a number:  & -8.0 & \hit{}-1680.0 \\
    6 & Give a number:  & -8.0 & -1680.0 \\
    3 & Give a number:  & \hit{}0.0 & -1680.0 \\
\end{longtable}

当变量的值发生变化时,会插入\hit{}宏。你可以自定义一些有意义的内容,比如一个颜色宏:\newcommand{\hit}{\color{red}}

1

你可以使用Python调试器,虽然我不知道怎么让它自动逐步执行,但这应该是可以做到的,然后你就可以解析输出结果。

这里有一个非常简单的例子:

adding.py

a = 1
b = 2

a = a + b

运行它...

PS >python -m pdb adding.py
> adding.py(1)<module>()
-> a = 1
(Pdb) alias stepprint step;;print a;;print b
(Pdb) stepprint
> adding.py(2)<module>()
-> b = 2
1
*** NameError: name 'b' is not defined
(Pdb) stepprint
> adding.py(4)<module>()
-> a = a + b
1
2
(Pdb) stepprint
--Return--
> adding.py(4)<module>()->None
-> a = a + b
3
2
(Pdb) stepprint
--Return--
> <string>(1)<module>()->None
3
2
(Pdb) stepprint
The program finished and will be restarted
> adding.py(1)<module>()
-> a = 1
*** NameError: name 'a' is not defined
*** NameError: name 'b' is not defined
(Pdb) q

PS >

在“程序结束”那一段按(q)结束。

11

目前的Python跟踪工具不支持这种用法,但应该可以自己构建一个。我不太清楚你是怎么决定输出哪些列的。在你的例子中,a和b是唯一的局部变量,但在更大的程序中,会有更多有趣的数据生态。

更新:这里有一个简单的概念验证:

 1     import sys
 2
 3     def trace(frame, event, arg_unused):
 4         print event, frame.f_lineno, frame.f_locals
 5         return trace
 6
 7     sys.settrace(trace)
 8
 9     def foo():
10         a = 1
11         b = 2
12
13         a = a + b
14
15     foo()

运行时,输出结果是:

call 9 {}
line 10 {}
line 11 {'a': 1}
line 13 {'a': 1, 'b': 2}
return 13 {'a': 3, 'b': 2}

撰写回答