Python抽象语法树

650 阅读4分钟

图片

抽象语法树(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进行可视化的信息打印,更为直观。通过linenocol_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]: aOut[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.rightOut[7]:(<_ast.Num at 0x10bc5b630>,<_ast.Add at 0x10a5e62b0>,<_ast.Num at 0x10bc5b240>)
In [8]: target.left.n, target.op, target.right.nOut[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某一个类型名字的方法,去访问对应结构类型的数据。这里是找到一个数字类型的节点,然后就把它给打印出来。

NodeTransformerNodeVisitor很像,但是它可以去修改节点,如替换或者删除旧的节点。如果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的使用。当我们去编译一个节点树的时候,编译器需要为支持它们的提供一个linenocol_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 1Found number 2Found 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]: aOut[7]: -3

以上就是本次分享的所有内容,想要了解更多 python 知识欢迎前往公众号:Python 编程学习圈 ,发送 “J” 即可免费获取,每日干货分享