Python抽象语法树


纸上得来终觉浅,绝知此事要躬行。

Python抽象语法树


抽象语法树(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]: 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某一个类型名字的方法,去访问对应结构类型的数据。这里是找到一个数字类型的节点,然后就把它给打印出来。

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

最后,补充一下相关的参考链接:


文章作者: Escape
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Escape !