Отслеживание Python выражение выражения шаг за шагом
Я пытаюсь написать визуализатор оценки выражений Python, который покажет, как выражения Python оцениваются шаг за шагом (в образовательных целях). Python Tutor от Philip Guo великолепен, но он оценивает программу 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, затем найти узел, который будет оцениваться первым, оценить его с помощью eval(compile(node, '', 'eval'))
(Я определенно не хочу переопределять весь Python:)), преобразовать результат оценки в AST Node (с помощью repr
а потом ast.parse()
?) и замените текущий узел на узел результата, затем используйте codegen.to_source
создать измененную строку кода из (модифицированного) AST и продолжать тот же процесс, пока у меня не будет только одного литерала в дереве.
Мой вопрос: как мне найти узел, который будет оценен первым? Кажется, что я могу пройти дерево в глубину с помощью подклассов ast.NodeVisitor
, но я не уверен, как я могу определить, что достиг нужного узла, и как мне прекратить обход после него?
РЕДАКТИРОВАТЬ.
Возможно, что мой первоначальный подход с преобразованием дерева неосуществим. На самом деле, элементарный шаг оценки выражения Python не обязательно должен быть заменой некоторого подвыражения на более простое (как в арифметике). Например, списочное понимание обеспечивает гораздо более сложное поведение, которое не может быть выражено в терминах, замените эту вещь этой вещью, а затем повторите рекурсивно. Поэтому я переформулирую вопрос немного. Мне нужен какой-то способ программно показать, как выражения Python оцениваются шаг за шагом. Например, функция трассировки MacroPy, упомянутая @jasonharper, является приемлемым решением на данном этапе. К сожалению, MacroPy, похоже, заброшен и не работает с Python 3. Есть ли идеи, как походить на это поведение трассировки в Python 3 без портирования полного MacroPy?
EDIT2.
Сразу после того, как я получил эту награду, я нашел похожий вопрос и отладчик с очень похожими функциями. Однако, поскольку нет окончательного ответа на этот вопрос, и мне не нужен полный отладчик, я все еще ищу ответ, который можно использовать, например, в среде Jupyter.
3 ответа
Степпинг выражений реализован в Thonny IDE.
Он использует инструментарий AST, где каждое (под) выражение e
превращается в after(before(<location info>), e)
, функции before
а также after
являются фиктивными функциями для вызова дополнительных call- событий в системе трассировки Python. Эти дополнительные вызовы уведомляют, когда (под) оценка выражения собирается начать или только что закончилась. (Подобные фиктивные функции добавляются для определения начала и конца каждого оператора.)
Инструментарий AST и интерпретация этих новых событий выполняются в thonny.backend.FancyTracer.
Узлы Python AST содержат начальную позицию соответствующих текстовых диапазонов, но иногда они неверны. Конечные позиции полностью отсутствуют. thonny.ast_utils.mark_text_ranges пытается позаботиться об этом (но решение на данный момент не завершено).
Было бы неплохо, если бы кто-то извлек соответствующую функциональность из Thonny в более общий пакет. Может быть, даже два пакета - один для вычисления информации о местоположении для Python AST и другой для детальной трассировки кода Python. Я был бы готов помочь с этим, если бы кто-то взял на себя инициативу.
Почему бы не использовать 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
Добавление двух списков, конечно, не первый узел, который будет оцениваться в этом коде; Я считаю, что на самом деле существует девять ранних оценок узлов - sorted
, 4
, 2
, 3
, 1
, [4,2,3,1]
, 5
, 6
, [5,6]
, Вам нужно будет не только определить, в каких оценках выполняются заказы, но и решить, какие из этих оценок стоит показать.
Я думаю, что лучшим подходом к вашей проблеме было бы изменение узлов AST, чтобы они испускали свое состояние до / после как побочный эффект от выполнения. Вас не заботит их порядок, вы просто выполните все выражение один раз. И уже есть пакет под названием macropy, у которого есть функция трассировки, которая делает именно это. Его вывод не совсем то, что вы просите, но, вероятно, его можно изменить, чтобы он стал ближе.