Таблица трассировки для программ Python

Есть ли способ получить таблицу трассировки для программы Python? Или для программы для запуска другой программы и получения ее таблицы трассировки? Я учитель, пытающийся безупречно проверить ответы на проблемы трассировки, которые мы используем в наших тестах.

Итак, например, если у меня есть программа Python с именем problem1.py со следующим содержимым:

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, и я не вижу способа его поддержки.


Обновление

Дамы и господа: используя превосходный совет Нед Батчелдер, я даю вам traceTable.py!

Ну.. почти. Как вы можете видеть в примере Ned Batchelder, frame.f_lineno не всегда ведет себя интуитивно (например, обе строки 3 и 4 подсчитываются как строка 4), но номера строк достаточно близки для довольно хорошей ссылки. Кроме того, все вычисления верны.

Я тестировал это с помощью длинной программы, содержащей оператор if, и он дал правильную таблицу (без номеров строк).

Вы также заметите, что моя программа значительно дольше, чем доказательство концепции Ned Batchelder из-за учета "более интересных экосистем данных" в более крупных программах, о которых он упомянул. В области использования execfile и всех переменных, необходимых для управления им и уменьшения шума (ala 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

Ответ 1

Это не тот случай, когда поддерживает текущий инструмент трассировки 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}

Ответ 2

Вы можете использовать отладчик 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) в бит "Готово к программе".

Ответ 3

Основываясь на том, что предложил ned-batchelder, как учитель, я создал класс Tracer, который помогает при создании LaTeX outputed longtable, показывая трассировку программы с выборочными переменными, минуя input() для (особенно при вызове макросом \bash из мощного пакета bashful LaTeX).

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}}