8

Is there a way to get the trace table for a Python program? Or for a program to run another program and get its trace table? I'm a teacher trying to flawlessly verify the answers to the tracing problems that we use on our tests.

So, for example, assuming I have a Python program named problem1.py with the following content:

problem1.py

 a = 1
 b = 2

 a = a + b

Executing the presumed program traceTable.py should go as:

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

(Or the same information with a different syntax)

I've looked into the trace module, and I can't see a way that it supports this.


Updated

Ladies and gentlemen: using Ned Batchelder's excellent advice, I give you traceTable.py!

Well.. almost. As you can see in Ned Batchelder's example, frame.f_lineno doesn't always behave intuitively (e.g. both lines 3 & 4 are counted as line 4), but the line numbers are close enough for a fairly good reference. Also, all calculations are correct.

I have tested this with a long program containing an if statement and it gave the correct table (sans the line numbers).

You will also notice that my program is significantly longer than Ned Batchelder's proof of concept due to accounting for the "more interesting ecosystems of data" in larger programs he mentioned. In the scope of using execfile and all the variables needed to manage it and reduce noise (ala ignored_variables) as well as produce proper string output, a lot more code is needed:

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

When called, it produces the output

 $ python traceTable.py problem1.py
 L || a | b
 2 || 1
 4 || 1 | 2
 4 || 3 | 2
Chris Redford
  • 16,982
  • 21
  • 89
  • 109
  • How does this work with things such as function calls? – Casebash Nov 08 '09 at 00:46
  • I'm teaching a very basic intro to computers class, so we don't actually use functions. But, based on Ned's proof of concept, my guess is that it would handle them normally. – Chris Redford Nov 08 '09 at 01:17
  • Doesn't work with python 3. Even after fixing `execfile`. Also tried using online converter. `KeyError: 'do_setlocale' – Robin Andrews Jul 03 '18 at 15:28

3 Answers3

11

This isn't a use case that the current Python tracing tool support, but it should be possible to build. I don't know how you decide what columns to output. In your sample, a and b are the only local variables, but larger programs would have more interesting ecosystems of data.

Updated: here's a simple proof of concept:

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

when run, the output is:

call 9 {}
line 10 {}
line 11 {'a': 1}
line 13 {'a': 1, 'b': 2}
return 13 {'a': 3, 'b': 2}
martineau
  • 119,623
  • 25
  • 170
  • 301
Ned Batchelder
  • 364,293
  • 75
  • 561
  • 662
  • Right. I was attempting to build it using exec statements but hit a major roadblock when it came to if statements and for loops. I have no idea how to intersperse the tracing in those situations. – Chris Redford Oct 29 '09 at 17:02
  • One thing that simplifies it, though, is that this is just an intro to computers class, so there would be no functions. Just one imperitive program. – Chris Redford Oct 29 '09 at 17:05
  • Excellent! Thank you so much! Using your answer, I am working on an update to the initial question that will produce the exact output specified originally. – Chris Redford Oct 29 '09 at 19:52
  • Same as above - errors with Python 3. It prints the trace, but then loads of other stuff and ends with `AttributeError: '_ModuleLock' object has no attribute 'name'`. – Robin Andrews Jul 03 '18 at 15:31
  • @Robin the identifier "name" doesn't appear in this code, so I'm not sure this code is to blame. You'll need to provide complete error reports and details of your code to get the problem resolved. – Ned Batchelder Jul 03 '18 at 19:02
  • I guess it's the way python 3 works. All I changed was adding brackets to the print statement. – Robin Andrews Jul 03 '18 at 22:33
1

You could use the Python debugger, though I do not know how to have it step through on it's own, but it's likely do-able, then you could just parse the output.

Here's a really crude example:

adding.py

a = 1
b = 2

a = a + b

running it...

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 >

End (q) on the "The program finished" bit.

Nick T
  • 25,754
  • 12
  • 83
  • 121
0

Based on what ned-batchelder proposed, as a teacher, I've made a Tracer class that help on creating LaTeX outputed longtable showing the trace of a program with selective variables, bypassing input() for automating process (especially while called by a \bash macro from the powerfull bashful LaTeX package).

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

Next you can find an exemple,and the output generated:

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

standard output: (while called by 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

output with Tracer: (while called by 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}

The \hit{} macro is insert while the value has changed. It's up to you to define something relevant, like a coloring macro : \newcommand{\hit}{\color{red}}

G. DAVID
  • 11
  • 2