逐步跟踪 Python 表达式求值

Tracing Python expression evaluation step by step

我正在尝试编写 Python 表达式评估可视化工具,它将显示如何逐步评估 Python 表达式(用于教育目的)。 Philip Guo 的 Python 导师很棒,但是它会逐行计算 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 节点(使用 repr 然后使用 ast.parse()?)并将当前节点替换为结果节点,然后使用 codegen.to_source 从(修改后的)AST 生成修改后的代码字符串并继续相同的过程,直到我在树中只有一个文字。

我的问题是:如何找到一个将被首先评估的节点?似乎我可以通过子类化 ast.NodeVisitor 优先遍历树,但我不确定如何检测到我到达了所需的节点以及如何在它之后停止遍历?


编辑。

我最初的树转换方法可能不可行。事实上,Python 表达式求值的基本步骤不一定是将某些子表达式替换为更简单的表达式(如算术)。例如,列表推导式提供了一种更复杂的行为,不能用术语 replace this thing by that thing, then repeat recursively 来表达。所以我稍微重申一下这个问题。我需要一些方法来以编程方式显示如何逐步评估 Python 表达式。例如,@jasonharper 提到的 MacroPy 的 tracing 功能是现阶段可以接受的解决方案。不幸的是,MacroPy 似乎已被放弃并且无法与 Python 一起使用 3. 是否有任何想法如何在不移植完整 MacroPy 的情况下类似于 Python 3 中的这种跟踪行为?


EDIT2.

在我授予此赏金之后,我发现 similar question and a debugger 具有非常接近的特征。但是,由于该问题没有最终答案,而且我不需要完整的调试器,我仍在寻找可以在 Jupyter 环境中使用的答案。

两个列表的相加当然不是该代码中要评估的第一个节点;我相信实际上有九个较早的节点评估 - sorted4231[4,2,3,1]56[5,6]。您不仅必须确定执行评估的顺序,还必须决定哪些评估值得显示。

我认为解决您的问题的更好方法是修改 AST 节点,以便它们发出 before/after 状态作为执行的副作用。你不会关心他们的顺序,你只需要执行一次整个表达式。并且已经有一个名为 macropy 的包具有跟踪功能,正是这样做的。它的输出并不完全符合您的要求,但可以将其修改为更接近的匹配。

为什么不使用 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

表达式步进在 Thonny IDE 中实现。

它使用 AST 工具,其中每个(子)表达式 e 被转换为 after(before(<location info>), e)。函数 beforeafter 是虚拟函数,用于在 Python 的跟踪系统中引起额外的 call 事件。这些额外的调用会在(子)表达式计算即将开始或刚刚结束时发出通知。 (添加了类似的虚拟函数来检测每个语句的开始和结束。)

这些新事件的 AST 检测和解释在 thonny.backend.FancyTracer 中完成。

Python的AST节点包含相应文本范围的起始位置,但有时不正确。结束位置完全缺失。 thonny.ast_utils.mark_text_ranges 试图解决这个问题(但目前解决方案不完整)。

如果有人将相关功能从 Thonny 提取到更通用的包中,那就太好了。甚至可能有两个包——一个用于计算 Python AST 的位置信息,另一个用于详细跟踪 Python 代码。如果有人带头,我愿意为此提供帮助。