给出两个生成相同抽象语法树(AST)的程序,是否保证它们在给定相同输入的情况下以相同的行为运行?
作为一个具体示例,我想对Python模块运行格式化程序以更改样式。为了检查格式化程序是否没有修改程序的逻辑,我想将格式化模块的AST与原始程序进行比较。这是一个好方法吗?
这个问题的非学问答案是肯定的,因为AST是编译器使用的中间表示形式; AST生成后,即用于生成字节码。一种简单的检查两个AST是否相同的方法是使用ast.dump
函数,然后将结果作为字符串进行比较。
时髦的答案是,这取决于您所说的“相同”是什么意思-具体来说,取决于您要比较以确定它们是否相同的两个AST的属性。
例如,ast.dump
和x = 1; raise ValueError()
编译为“相同的” AST:
x = 1\nraise ValueError()
但是,AST还包括有关行号和位置的元数据,因此这两个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.parse('x = 1; raise ValueError()').body[1].lineno
1
>>> ast.parse('x = 1\nraise ValueError()').body[1].lineno
2
,第二个说line 1
:
line 2
从技术上讲,代码还可以检查错误消息中的行号,然后根据该行号决定其行为。 任何这样的代码都是可憎的,应该撤回并开枪射击,但是作为认证的学步车,我有责任注意这样的代码可以存在。
因此,从技术上讲,您的代码格式化程序不会导致真正“完全相同”的AST,因为它们的行/位置元数据可能会有所不同-您的代码格式化程序必须更改该元数据才能执行任何有用的操作。但这对于像您这样的自动代码格式设置工具来说是一个合理的警告,因为编写代码的人在重新设置格式时会中断,他们应该知道他们的代码太脆弱而无法被自动工具重新格式化。
为了完整性,如果要确保编译的字节码相同,则可以使用>>> 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
函数:这比dis.get_instructions
更为严格,因为字节码包含行号(但不包含行内的位置) ),但是如果格式化程序不应该在不同行之间移动代码,那么您可能更喜欢这种方式。
dis.get_instructions