纸上得来终觉浅,绝知此事要躬行。
抽象语法树(AST
),即Abstract Syntax Tree
的缩写。它是源代码语法结构的一种抽象表示。它以树状的形式表现编程语言的语法结构,树上的每个节点都表示源代码中的一种结构。之所以说语法是抽象的,是因为这里的语法并不会表示出真实语法中出现的每个细节。
在Python
中,使用ast
模块获取对应Python
代码的抽象语法树。当然,也可以修改语法的执行效果。
下面我们看一下实际的例子,来进行说明和解释。通过ast
模块接收执行计算内容,解析之后的结果就是一个语法树。这个语法树在body
里面,对应了不同的结构类型的语法。其中Expr
就是表达式,BinOp
表示运算符,Num
就表达一个数字,op
表示需要操作的运算符类型。
(1 + 2) * 3
In [1]: import ast
In [2]: ast.dump(ast.parse('(1 + 2) * 3'))
Out[2]: 'Module(body=[Expr(value=BinOp(left=BinOp(left=Num(n=1), op=Add(), right=Num(n=2)), op=Mult(), right=Num(n=3)))])'
上面的抽象语法树输出不太友好,可以使用第三方的库astpretty
进行可视化的信息打印,更为直观。通过lineno
和col_offset
来描述,代码所在的行数和偏移位置。
In [1]: astpretty.pprint(ast.parse('(1 + 2) * 3').body[0], indent=' ')
Expr(
lineno=1,
col_offset=0,
value=BinOp(
lineno=1,
col_offset=0,
left=BinOp(
lineno=1,
col_offset=1,
left=Num(lineno=1, col_offset=1, n=1),
op=Add(),
right=Num(lineno=1, col_offset=5, n=2),
),
op=Mult(),
right=Num(lineno=1, col_offset=10, n=3),
),
)
这个抽象语法树是可以被编译和求值的,也可以深入语法树找到对应子树中某一个节点结构的值和类型。这里,可以通过compile
将其编译成为一个code
的对象,也可以通过eval
去执行,结果为a=9
。
当然,这里也是可以获取到对应子树的节点和值的,由astpretty
模块输出的抽象树,找到对应的位置,进行输出。
In [1]: compile(ast.parse('a = (1 + 2) * 3'), '<input>', 'exec')
Out[1]: <code object <module> at 0x10ba4aa50, file "<input>", line 1>
In [2]: eval(compile(ast.parse('a = (1 + 2) * 3'), '<input>', 'exec'))
In [3]: a
Out[3]: 9
In [4]: tree = ast.parse('a = (1 + 2) * 3')
In [5]: body = tree.body[0]
In [6]: target = body.value.left
In [7]: target.left, target.op, target.right
Out[7]:
(<_ast.Num at 0x10bc5b630>,
<_ast.Add at 0x10a5e62b0>,
<_ast.Num at 0x10bc5b240>)
In [8]: target.left.n, target.op, target.right.n
Out[8]: (1, <_ast.Add at 0x10a5e62b0>, 2)
到这里我们已经了解到了ast
模块的基本用法,而它能够干什么呢?其实它有很多的应用,如代码检查(其实pylint
就是基于ast
的)、语法高亮、代码压缩(其实就是通过语法树实现的)、关键字匹配等等。
之前说过ast
会将代码抽象成一个语法树,而开发者是由绝地的权限去操作的,可以修改、删除部分子树的逻辑。这里演示一个把加号改为减号的示例。
In [1]: x = ast.parse('1 + 1', mode='eval')
In [2]: x.body.op = ast.Sub()
In [3]: eval(compile(x, '<string>', 'eval'))
Out[3]: 0
但是,最好的写法是下面这种写法。这里继承了两个类,第一个是NodeVisitor
,它会遍历抽象语法树,可以通过visit_Num
某一个类型名字的方法,去访问对应结构类型的数据。这里是找到一个数字类型的节点,然后就把它给打印出来。
而NodeTransformer
和NodeVisitor
很像,但是它可以去修改节点,如替换或者删除旧的节点。如果visit_Add
返回None
表示删除对应的节点,否则将替换为返回值。
import ast
class MyVisitor(ast.NodeVisitor):
def visit_Num(self, node):
print(f'Found number {node.n}')
class MyTransformer(ast.NodeTransformer):
def visit_Add(self, node):
return ast.Sub()
使用MyVisitor().visit(node)
的时候,返回对于节点的值,而使用MyTransformer().visit(node)
并没有返回值而是改变了操作符类型,由加号变成减号,所以最后的结果为-3
。
另外,我们注意这个fix_missing_locations
的使用。当我们去编译一个节点树的时候,编译器需要为支持它们的提供一个lineno
和col_offset
来表示它们的位置。而Python
又是一个非常注意代码缩进的语言,所以使用fix_missing_locations
来自动补齐缩进功能。
In [1]: from ast_example import MyVisitor, MyTransformer
In [2]: node = ast.parse('a = (1 + 2) * 3')
In [3]: MyVisitor().visit(node)
Found number 1
Found number 2
Found number 3
In [4]: MyTransformer().visit(node)
Out[4]: <_ast.Module at 0x10cb7c198>
In [5]: node = ast.fix_missing_locations(node)
In [6]: exec(compile(node, '<string>', 'exec'))
In [7]: a
Out[7]: -3
最后,补充一下相关的参考链接: