逐步跟踪Python表达式评估

时间:2016-11-16 18:30:57

标签: python abstract-syntax-tree

我正在尝试编写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,然后找到将首先评估的节点,使用eval(compile(node, '', 'eval'))进行评估(我绝对不想重新实现整个Python :)),将评估结果转换为AST节点(使用repr然后ast.parse()?)并用结果节点替换当前节点,然后使用codegen.to_source生成修改后的代码来自(已修改)AST的字符串并继续相同的过程,直到我在树中只有一个文字。

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

修改

我对树的转换的初始方法可能是不可行的。事实上,评估Python表达式的基本步骤不是必须将某个子表达式替换为更简单的表达式(如算术中)。例如,列表推导提供了一个更复杂的行为,无法用术语表示用该东西替换这个东西,然后递归地重复。所以我稍微重申了一个问题。我需要一些方法来programmaticaly显示如何逐步评估Python表达式。例如,@ jasonharper提到的MacroPy的tracing功能在此阶段是可接受的解决方案。不幸的是,MacroPy似乎被放弃了,并且不适用于Python 3.在没有移植完整的MacroPy的情况下,有没有想法如何在Python 3中类似这种跟踪行为?

EDIT2

在我获得此奖励之后,我发现similar questiondebugger具有非常接近的特征。但是,由于这个问题没有最终答案,而且我不需要完整的调试器,我仍然在寻找可以在Jupyter环境中使用的答案。

3 个答案:

答案 0 :(得分:6)

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

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

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

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

如果有人从Thonny中提取相关功能到一个更通用的包,那将是很好的。甚至可能是两个包 - 一个用于计算Python AST的位置信息,另一个用于详细跟踪Python代码。如果有人带头,我愿意帮助解决这个问题。

答案 1 :(得分: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

答案 2 :(得分:1)

添加两个列表肯定不是该代码中要评估的第一个节点;我相信事实上有九个早期节点评估 - sorted4231[4,2,3,1],{{1 },56。您不仅需要确定执行哪些订单评估,还必须决定哪些评估值得展示。

我认为更好的方法是修改AST节点,使它们发出前/后状态作为执行的副作用。你不会关心他们的顺序,你只需执行一次整个表达式。并且已经有一个名为macropy的程序包具有跟踪功能,可以完成此操作。它的输出并不是你所要求的,但它可能会被修改为更接近的匹配。