相同的抽象语法树是否保证相同的行为?

Do identical Abstract Syntax Trees guarantee the same behaviour?

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

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

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


迂腐的回答是,这取决于您所说的 "identical" 的意思——具体来说,您希望比较两个 AST 的哪些属性以确定它们是否相同。

例如,x = 1; raise ValueError()x = 1\nraise ValueError() 编译为 "identical" ASTs:

>>> 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

从技术上讲,代码也可以检查错误消息中的行号,然后据此决定其行为。 任何这样的代码都是令人厌恶的,应该被取出来枪毙,但作为一个经过认证的学究,我有责任注意这样的代码可能存在。

所以从技术上讲,您的代码格式化程序不会产生真正的 "identical" AST,因为它们的 line/position 元数据可能不同 - 您的代码格式化程序必须更改该元数据才能执行任何有用的操作。但对于像您这样的自动代码格式化工具来说,这是一个合理的警告,因为编写的代码在您重新格式化时会中断的人应该知道他们的代码太脆弱了由自动工具重新格式化。


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

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