第 9 章 抽象语法树

抽象语法树(Abstract Syntax Tree,AST)是任何语言源代码的抽象结构的树状表示,包括Python语言。作为Python自己的抽象语法树,它是基于对Python源文件的解析而构建的。

关于Python的这个部分并没有太多的文档,而且刚开始看的时候并不容易理解。尽管如此,Python作为一门编程语言,要掌握它的用法,了解并理解Python的一些深层次的构造仍然是很有意义的。

广告:个人专属 VPN,独立 IP,无限流量,多机房切换,还可以屏蔽广告和恶意软件,每月最低仅 5 美元

了解Python抽象语法树的最简单的方式就是解析一段Python代码并将其转储从而生成抽象语法树。要做到这一点,Python的ast模块就可以满足需要。

示例 9.1 Python 代码解析成抽象语法树

>>> import ast
>>> ast.parse
<function parse at 0x7f062731d950>
>>> ast.parse("x = 42")
<_ast.Module object at 0x7f0628a5ad10>
>>> ast.dump(ast.parse("x = 42"))
"Module(body=[Assign(targets=[Name(id='x', ctx=Store())], value=Num(n=42))])"

ast.parse函数会返回一个_ast.Module对象,作为树的根。这个树可以通过ast.dump模块整个转储,针对这个例子如图9-1所示。

抽象语法树的构建通常从根元素开始,根元素通常是一个ast.Module对象。这个对象在其body属性中包含一组待求值的语句或者表达式。它通常表示这个文件的内容。

很容易猜到,ast.Assign对象表示赋值,在Python语法中它对应=Assign有一组目标,以及一个要赋的值。在这个例子中只有一个对象ast.Name,表示变量x。值是数值42

阅读 ‧ 电子书库

图9-1 

抽象语法树能够被传入Python并编译和求值。作为Python内置函数提供的compile函数是支持抽象语法树的。

>>> compile(ast.parse("x = 42"), '<input>', 'exec')
<code object <module> at 0x111b3b0, file "<input>", line 1>
>>> eval(compile(ast.parse("x = 42"), '<input>', 'exec'))
>>> x
42

通过ast模块中提供的类可以手工构建抽象语法树。显然,这么写Python代码太麻烦了,不推荐这种方法。但是用起来还是挺有意思的。

让我们用Python的抽象语法树写一个经典的“Hello world!”。

示例 9.2  使用 Python 抽象语法树的 Hello world

>>> hello_world = ast.Str(s='hello world!', lineno=1, col_offset=1)
>>> print_call = ast.Print(values=[hello_world], lineno=1, col_offset=1, nl=True)
>>> module = ast.Module(body=[print_call])
>>> code = compile(module, '', 'exec')
>>> eval(code)
hello world!

 注意

linenocol_offset表示用于生成抽象语法树的源代码的行号和列偏移量。在当前环境下设置它们其实没多大意义,因为这里并没有解析任何源代码,但用来回查生成抽象语法树的代码的位置时比较有用。例如,在Python生成栈回溯时就比较有用。不管怎样,Python拒绝编译任何不提供此信息的抽象语法树对象,这也是我们在这里传入一个假的值1的原因。ast.fix_missing_locations()函数可以通过在父节点上设置缺失的值来解决这个问题。

抽象语法树中可用的完整对象列表通过阅读_ast(注意下划线)模块的文档可以很容易获得。

首先需要考虑的两个分类是语句和表达式。语句涵盖的类型包括assert(断言)、赋值(=)、增量赋值(+=/=等)、globaldefifreturnforclasspassimport等。它们都继承自ast.stmt。表达式涵盖的类型包括lambdanumberyieldname(变量)、compare或者call。它们都继承自ast.expr

还有其他一些分类,例如,ast.operator用来定义标准的运算符,如加(+)、除(/)、右移(<<)等,ast.cmpop用来定义比较运算符。

很容易联想到有可能利用抽象语法树构造一个编译器,通过构造一个Python抽象语法树来解析字符串并生成代码。这正是9.1节中将要讨论的Hy项目。

如果需要遍历树,可以用ast.walk函数来做这件事。但ast模块还提供了NodeTransformer,一个可以创建其子类来遍历抽象语法树的某些节点的类。因此用它来动态修改代码很容易。为加法修改所有的二元运算如示例9.3所示。

示例 9.3 为加法修改所有的二元运算

import ast

class ReplaceBinOp(ast.NodeTransformer):
  """Replace operation by addition in binary operation"""
  def visit_BinOp(self, node):
    return ast.BinOp(left=node.left,
             op=ast.Add(),
             right=node.right)

tree = ast.parse("x = 1/3")
ast.fix_missing_locations(tree)
eval(compile(tree, '', 'exec'))
print(ast.dump(tree))
print(x)
tree = ReplaceBinOp().visit(tree)
ast.fix_missing_locations(tree)
print(ast.dump(tree))
eval(compile(tree, '', 'exec'))
print(x)

执行结果如下:

Module(body=[Assign(targets=[Name(id='x', ctx=Store())],
          value=BinOp(left=Num(n=1), op=Div(), right=Num(n=3)))])
0.33 33333333333333
Module(body=[Assign(targets=[Name(id='x', ctx=Store())],
          value=BinOp(left=Num(n=1), op=Add(), right=Num(n=3)))])
4

 提示

如果需要对Python的字符串进行求值并返回一个简单的数据类型,可以使用ast.literal_eval。与eval不同,ast.literal_eval不允许输入的字符串执行任何代码。比eval更安全。