Трассировка оценки экспрессии Python шаг за шагом

Я пытаюсь написать визуализатор оценки экспрессии Python, который покажет, как выражения Python оцениваются шаг за шагом (для образовательных целей). Philip Guo Python Tutor отлично работает, но он оценивает программу Python по очереди, и я обнаружил, что студенты иногда не понимают, как оцениваются однострочные выражения, такие как sorted([4, 2, 3, 1] + [5, 6])[1] == 2, и я хотел бы визуализировать этот процесс. (Похоже, что никто этого не делал - по крайней мере, я ничего не нашел.) Идеальное решение создаст последовательность таких строк:

sorted([4, 2, 3, 1] + [5, 6])[1] == 2
sorted( >> [4, 2, 3, 1] + [5, 6] << )[1] == 2
>> sorted([4, 2, 3, 1, 5, 6]) << [1] == 2
>> [1 2 3 4 5 6][1] << == 2
>> 2 == 2 <<
True

Здесь >> и << используются для выделения части выражения, которое оценивается на текущем шаге, а затем заменяется на его значение. (Возможно, я попытаюсь преобразовать эту последовательность в некоторую анимацию позже.)

Моя текущая стратегия - использовать ast.parse() для синтаксического анализа строки в AST, а затем найти node, который будет оцениваться первым, оценить его с помощью eval(compile(node, '', 'eval')) (я определенно не хочу переопределять весь Python:)), преобразуйте результат оценки в AST node (с repr, а затем ast.parse()?) и замените текущий node на результат node, затем используйте codegen.to_source для создания модифицированной строки кода из (изменено) AST и продолжить тот же процесс, пока в дереве не будет только одного литерала.

Мой вопрос: как я могу найти node, который будет оцениваться первым? Кажется, что я могу пересекать глубину дерева - сначала с помощью подкласса ast.NodeVisitor, но я не уверен, как я могу обнаружить, что достиг желаемого node, и как я могу остановить перемещение после него?


ИЗМЕНИТЬ.

Возможно, мой первоначальный подход с преобразованием дерева невозможен. Фактически, элементарный шаг оценки выражения Python не обязательно должен быть заменой некоторого подвыражения более простому (как в арифметике). Например, понимание списков обеспечивает гораздо более сложное поведение, которое не может быть выражено в терминах, заменяющих эту вещь этой штукой, а затем повторяется рекурсивно. Поэтому я немного переформулирую вопрос. Мне нужно каким-то образом программно показать, как выражения Python оцениваются шаг за шагом. Например, функция MacroPy tracing, упомянутая @jasonharper, является приемлемым решением на данном этапе. К сожалению, MacroPy, кажется, оставлен и не работает с Python 3. Есть ли какие-либо идеи, как напоминать это поведение трассировки в Python 3 без портирования полного MacroPy?


EDIT2.

Сразу после того, как я наградил эту награду, я нашел аналогичный вопрос и отладчик с очень близкими функциями. Однако, поскольку нет окончательного ответа на этот вопрос, и мне не нужен полный отладчик, я все еще ищу ответ, который можно использовать, например, в среде Jupyter.

Ответ 1

Степень вытеснения реализована в Thonny IDE.

Он использует прибор AST, где каждое (суб) выражение e преобразуется в after(before(<location info>), e). Функции before и after являются фиктивными функциями, вызывающими дополнительные события вызова в системе трассировки Python. Эти дополнительные вызовы уведомляют, когда оценка выражения (под) выражается в начале или только что закончилась. (Аналогичные фиктивные функции добавляются для определения начала и конца каждого оператора.)

АСТ-приборы и интерпретация этих новых событий выполняются в thonny.backend.FancyTracer.

Узлы Python AST содержат начальную позицию соответствующих диапазонов текста, но иногда они неверны. Конечные позиции полностью отсутствуют. thonny.ast_utils.mark_text_ranges пытается позаботиться об этом (но на данный момент решение является неполным).

Было бы неплохо, если бы кто-то извлек соответствующую функциональность из Thonny в более общий пакет. Возможно, даже два пакета - один для вычисления информации о местоположении для Python AST и других для подробного отслеживания кода Python. Я был бы готов помочь в этом, если бы кто-то взял на себя инициативу.

Ответ 2

Почему бы не использовать модуль dis?

Так как CPython компилирует Python в байт-код и запускает его, просмотр байт-кода дает вам наилучшее представление о том, что на самом деле происходит.

In [1]: import dis

In [2]: dis.dis('sorted([4, 2, 3, 1] + [5, 6])[1] == 2')
  1           0 LOAD_NAME                0 (sorted)
              3 LOAD_CONST               0 (4)
              6 LOAD_CONST               1 (2)
              9 LOAD_CONST               2 (3)
             12 LOAD_CONST               3 (1)
             15 BUILD_LIST               4
             18 LOAD_CONST               4 (5)
             21 LOAD_CONST               5 (6)
             24 BUILD_LIST               2
             27 BINARY_ADD
             28 CALL_FUNCTION            1 (1 positional, 0 keyword pair)
             31 LOAD_CONST               3 (1)
             34 BINARY_SUBSCR
             35 LOAD_CONST               1 (2)
             38 COMPARE_OP               2 (==)
             41 RETURN_VALUE

Изменить. Альтернативный метод может состоять в том, чтобы показать шаги один за другим в IPython:

In [1]: [4, 2, 3, 1]
Out[1]: [4, 2, 3, 1]

In [2]: [4, 2, 3, 1] + [5, 6]
Out[2]: [4, 2, 3, 1, 5, 6]

In [3]: sorted([4, 2, 3, 1, 5, 6])
Out[3]: [1, 2, 3, 4, 5, 6]

In [4]: [1, 2, 3, 4, 5, 6][1]
Out[4]: 2

In [5]: 2 == 2
Out[5]: True

Ответ 3

Добавление двух списков, конечно, не является первым node, которое должно быть оценено в этом коде; Я считаю, что на самом деле существует девять ранних оценок node - sorted, 4, 2, 3, 1, [4,2,3,1], 5, 6, [5,6]. Мало того, что вам нужно будет определить, какие оценки заказа выполняются, вам также нужно будет решить, какая из этих оценок стоит показать.

Я думаю, что лучший подход к вашей проблеме состоял бы в том, чтобы изменить узлы AST, чтобы они излучали их до/после состояния как побочный эффект выполнения. Вы не заботитесь о своем заказе, вы просто выполняете все выражение один раз. И уже есть пакет под названием macropy, в котором есть функция трассировки, которая делает именно это. Его вывод не совсем то, о чем вы просите, но его, вероятно, можно было бы изменить, чтобы быть более близким.