首页 > 解决方案 > 相同的抽象语法树是否保证相同的行为?

问题描述

给定两个生成相同抽象语法树 (AST) 的程序,它们是否保证在相同输入的情况下以相同的行为运行?

举一个具体的例子,我想对 Python 模块运行格式化程序以更改样式。为了检查格式化程序没有修改程序的逻辑,我想比较格式化模块的AST和原始的。这是一个好方法吗?

标签: pythonformattingprogramming-languagesabstract-syntax-tree

解决方案


对这个问题的非迂腐回答是肯定的,因为 AST 是编译器使用的中间表示;一旦生成了 AST,这就是用来生成字节码的东西。检查两个 AST 是否相同的一种简单方法是使用该ast.dump函数,然后将结果作为字符串进行比较。


迂腐的答案是,这取决于您所说的“相同”是什么意思 - 具体来说,您希望比较两个 AST 的哪些属性以确定它们是否相同。

例如,x = 1; raise ValueError()x = 1\nraise ValueError()编译为“相同”的 AST:

>>> import ast
>>> print(ast.dump(ast.parse('x = 1; raise ValueError()')))
Module(body=[
  Assign(targets=[Name(id='x', ctx=Store())], value=Num(n=1)),
  Raise(exc=Call(func=Name(id='ValueError', ctx=Load()), args=[], keywords=[]), cause=None)
])
>>> print(ast.dump(ast.parse('x = 1\nraise ValueError()')))
Module(body=[
  Assign(targets=[Name(id='x', ctx=Store())], value=Num(n=1)),
  Raise(exc=Call(func=Name(id='ValueError', ctx=Load()), args=[], keywords=[]), cause=None)
])

但是,AST 还包含有关行号和位置的元数据,因此这两个 AST 并不完全相同:

>>> ast.parse('x = 1; raise ValueError()').body[1].lineno
1
>>> ast.parse('x = 1\nraise ValueError()').body[1].lineno
2

此外,这些行号在运行时在错误消息中可用;第一个说line 1,第二个说line 2

>>> exec('x = 1; raise ValueError()')
Traceback (most recent call last):
  File "<pyshell#13>", line 1, in <module>
    exec('x = 1; raise ValueError()')
  File "<string>", line 1, in <module>
ValueError
>>> exec('x = 1\nraise ValueError()')
Traceback (most recent call last):
  File "<pyshell#14>", line 1, in <module>
    exec('x = 1\nraise ValueError()')
  File "<string>", line 2, in <module>
ValueError

从技术上讲,代码也可以检查错误消息中的行号,然后据此决定其行为。任何这样的代码都是可憎的,应该被拿出来并枪毙,但作为一个经过认证的书呆子,我有责任注意到这样的代码是可以存在的。

因此,从技术上讲,您的代码格式化程序不会产生真正“相同”的 AST,因为它们的行/位置元数据可能不同 - 您的代码格式化程序必须更改该元数据才能做任何有用的事情。但是,对于像您这样的自动代码格式化工具来说,这是一个合理的警告,因为编写在您重新格式化时会中断的代码的人应该知道他们的代码太脆弱而无法被自动工具重新格式化。


为了完整起见,如果您想确保编译的字节码是相同的,您可以使用该dis.get_instructions函数:这是一个更严格的检查,ast.dump因为字节码包含行号(但不包括行内的位置),但如果您的格式化程序不应该t 在不同的行之间移动代码,那么您可能更喜欢这种方式。

>>> import dis
>>> instructions1 = list(dis.get_instructions('x = 1; y = 2'))
>>> instructions2 = list(dis.get_instructions('x = 1\ny = 2'))
>>> instructions1 == instructions2
False

推荐阅读