一、简介
正如StackOverflow所明确指出的,Python 的受欢迎程度近年来急剧上升。因此,更多的软件工具需要能够读取和分析 Python 代码。开源的 ast 包为此提供了很多能力,本文的目的就是介绍它的特性。
AST 代表抽象语法树,后面的部分将解释这些树是什么以及它们为何重要。ast 包使得将 Python 代码读入 AST 成为可能,并且 AST 的每个节点都由不同的 Python 类表示。
本文的最后一部分讨论了 astor(AST 观察/重写)包。这为读写 AST 提供了有用的功能。
2. 两个基本功能
在深入了解 Python 分析的细节之前,我想先从一个简单的例子开始,说明为什么 ast 包如此有用。有两个基本功能需要了解:
parse(code_str)
– 从包含 Python 代码的字符串创建抽象语法树dump(node, annotate_fields=True, include_attributes=False, *, indent=None)
– 将抽象语法树转换为字符串
为了演示如何使用这些方法,下面的代码调用ast.parse
为一个简单的for
循环创建一个抽象语法树。然后它调用ast.dump
将 转换tree
为字符串。
tree = ast.parse("for i in range(10):\n\tprint('Hi there!')") print(ast.dump(tree, indent=4))
执行此代码时,会产生以下结果:
Module( body=[ For( target=Name(id='i', ctx=Store()), iter=Call( func=Name(id='range', ctx=Load()), args=[ Constant(value=10)], keywords=[]), body=[ Expr( value=Call( func=Name(id='print', ctx=Load()), args=[ Constant(value='Hi there!')], keywords=[]))], orelse=[])], type_ignores=[])
对于一个随便的程序员来说,这可能看起来一团糟。但对于任何试图构建分析 Python 代码的工具的人来说,这都很重要。这个输出标识了代码的结构,结构以抽象语法树的形式给出。
3. 抽象语法树(AST)
为了理解解析器的结果,理解抽象语法树或 AST 很重要。这些树体现了文档内容的结构,无论它是用 Python 等编程语言还是英语等自然语言编写的。
本节解释什么是 AST,然后介绍代表 AST 节点的类。但在介绍抽象语法树之前,我想先退一步解释一下什么是树结构。
3.1 树结构
当数据元素形成以单个元素开始的层次结构时,元素及其关系可以表示为树。常见的树包括组织结构图、文件导航器和家谱。树结构在软件开发中经常遇到,特别是在网络、图形和文本分析中。
在使用树时,开发人员依赖于一组通用术语:
- 树中的每个元素称为一个节点。
- 最顶层的元素称为根节点。
- 如果一个节点连接到它下面的节点,则第一个节点称为父节点,连接的节点是父节点的子节点。
- 除根外的每个节点都有一个父节点。有一个或多个子节点的节点称为分支节点,没有子节点的节点称为叶节点。
图 1 描绘了一个简单的树。节点 E 是根节点,节点 B、C 和 D 是其子节点。节点 A 和 F 是 B 的子节点,节点 G 是 D 的子节点。节点 A、F、C 和 G 没有子节点,因此它们是叶节点。其他节点是分支节点。

图 1:一个简单的树层次结构
树中的每个节点都有一个深度值,该值标识有多少连接将其与根分开。在此示例中,节点 E 的深度为 0,节点 C 的深度为 1,节点 G 的深度为 2。
3.2 抽象语法树(AST)
当我上小学的时候,我们不得不使用句子树来分析句子。根节点代表整个句子,每个根节点有两个孩子:一个用于主语,一个用于谓语。在一个简单的句子中,主语由名词短语表示,谓词由动词短语表示。图 2 显示了句子的树:This sentence is simple。

图 2:示例句子树
在这棵树中,叶子节点包含组成文本的各个字符串。分支节点识别每个叶节点的用途以及它在句子中所起的作用。
如果你能看到句子树是如何表示英语句子的,那么你就不会很难理解抽象语法树是如何表示用 Python 编写的代码的。在ast.parse
分析 Python 代码时,根节点采用以下四种形式之一:
- 模块– 语句集合
- function – 函数的定义
- 交互式– 交互式会话中的语句集合
- 表达式– 简单表达式
图 3 说明了for
前面介绍的简单 Python 循环的 AST。根节点是一个模块。

图 3:Python AST 示例
我遇到的几乎每个 Python AST 都有一个模块作为其根节点。一个模块由一个或多个语句组成,大多数类型的语句由一个或多个表达式组成。以下讨论探讨了语句和表达式的主题。
3.2.1 声明
在前面的 AST 中,模块包含一个表示for
循环的语句。除了for
循环,AST 语句还可以表示函数定义、类定义、while
循环、if
语句、return
语句和import
语句。
每个语句节点都有一个或多个子节点,其子节点的数量和类型的变化取决于语句的类型。例如,一个函数定义至少有四个孩子:一个标识符、参数、一个装饰器列表和一组构成其主体的语句。为了看到这一点,下面的代码解析 Python 代码,该代码定义了一个名为foo
.
tree = ast.parse("def foo():\n\tprint('Hello!')") print(ast.dump(tree, indent=4))
第二行从 AST 创建以下字符串:
Module( body=[ FunctionDef( name='foo', args=arguments( posonlyargs=[], args=[], kwonlyargs=[], kw_defaults=[], defaults=[]), body=[ Expr( value=Call( func=Name(id='print', ctx=Load()), args=[ Constant(value='Hello!')], keywords=[]))], decorator_list=[])], type_ignores=[])
从左到右看,很明显根节点是一个模块,它的子节点是一个函数定义。函数定义有四个孩子,代表主体的孩子有一个孩子,因为函数的主体包含一行代码。
类定义尤为重要,每个子类都有五个子类:一个名称、零个或多个基类、零个或多个关键字、零个或多个语句以及零个或多个装饰器。类中的每个方法都由一个函数定义语句表示。
为了证明这一点,请考虑以下简单的类定义:
class Example: def __init__(self): self.prop = 4 def printProp(self): print(self.prop)
以下代码解析此类定义以获取 AST。
tree = ast.parse("class Example:\n\tdef __init__(self):\n\t\tself.prop = 4\n\n\tdef printProp(self):\n\t\tprint(self.prop)")
图 4 展示了它的顶级节点,而不是打印出整个 AST。

图 4:类定义的 AST
许多语句,例如return
语句和import
语句,都非常简单。但其他语句,例如语句和赋值语句,由称为表达式if
的子结构组成。我接下来会讨论这些。
3.2.2 表达式
我们都熟悉 2+2 和 8*9 等数学表达式,但 Python AST 中的表达式更难确定。语句和表达式之间没有明显的区别,实际上,表达式可以是语句。在 Python AST 中,表达式可以采用多种不同形式中的一种,包括以下形式:
- 二元、一元和布尔运算
- 涉及值和容器的比较
- 函数调用(不是函数定义)
- 容器(列表、元组、字典、集合)
- 属性、下标和切片
- 常量和名称(字符串)
最后一个子弹很重要。几乎 AST 中的每个叶节点都是名称或常量,因此区分这两个表达式很重要。名称是一个标识符,例如函数名、类名或变量名。常量是不是标识符的任何值。
要了解如何解析表达式,看一个示例会有所帮助。以下代码解析一个简单的数学表达式并打印其 AST。
tree = ast.parse("(x+3)*5") print(ast.dump(tree, indent=4))
打印的 AST 如下所示:
Module( body=[ Expr( value=BinOp( left=BinOp( left=Name(id='x', ctx=Load()), op=Add(), right=Constant(value=3)), op=Mult(), right=Constant(value=5)))], type_ignores=[])
该模块包含一个语句,该语句是一个表达式。该表达式由两个二元运算组成:加法和乘法。变量 x 由名称节点标识,两个数值由值节点标识。
3.2 AST 类
Python AST 中的每个节点类型在 ast 包中都有一个对应的类。模块由Module
类的实例表示,表达式由Expr
实例表示。函数定义由FunctionDef
实例表示,类定义由ClassDef
实例表示。
节点的每个子节点对应于相应类的一个属性。在图 4 中,类定义节点具有名为 name、body、bases、keywords 和 decorator list 的子节点。为了存储此信息,ClassDef
该类具有名为name
、body
、bases
、keywords
和的属性decorator_list
。
每个节点类都从中心AST
类扩展而来。这有一些有用的属性可以提供有关节点的信息:
_fields
– 一个包含节点子节点名称的元组(对应于类属性)lineno
– 包含节点的第一行号endlineno
– 包含节点的最后一行号colno
– 包含节点的第一列endcolno
– 包含节点的最后一列
例如,以下代码列出了if
语句的子项:
print(ast.If._fields)
打印输出为('test', 'body', 'orelse')
.
关于节点类及其构造函数的文档并不多。但是您可以通过查看dump
方法的输出来了解节点是如何构造的。要将此输出转换为构造函数,只需在每个节点类前面加上ast
前缀即可。例如,下面的代码依赖于前面的输出来定义一个包含两个二元运算的表达式:
firstOp = ast.BinOp(left=ast.BinOp(left=ast.Name(id='x', ctx=ast.Load()), op=ast.Add(), right=ast.Constant(value=3))) secondOp = ast.Mult() e = ast.Expr(value=firstOp, op=secondOp, right=ast.Constant(value=5))
一旦您了解了如何实例化节点类,您就可以以编程方式构建 AST。然后,您可以使用 ASTOR 包从 AST 生成 Python 代码,我将在下面讨论。
4. 使用 ASTOR
为了增强 ast 包的功能,Berker Peksag 发布了astor,它代表 AST Observe/Rewrite。如果你有 pip 可用,你可以使用命令安装 astor pip install astor
。在撰写本文时,当前版本为 0.8.1。
astor 提供了许多有用的类和函数来简化 Python AST 的使用。表 1 列出了其中六个功能并提供了每个功能的描述。
功能 | 描述 |
---|---|
to_source(ast, indent_with=' '*4, add_line_information=False) | 将 AST 转换为 Python 代码 |
code_to_ast(codeobj) | 将模块重新编译为 AST 并 为函数提取子 AST |
parse_file(file) | 将 Python 文件解析为 AST |
dump_tree(node, name=None, initial_indent='', indentation=' ', maxline=120, maxmerged=80) | 漂亮地打印带有缩进的 AST |
strip_tree(node) | 从 AST 中递归删除属性 |
iter_node(node, unknown=None) | 迭代一个 AST 节点 |
第一个函数 ,to_source
特别有用,因为它接受 AST(或节点)并打印 Python 代码。为了演示这一点,以下代码调用ast.parse
以获取函数定义的 AST。然后它调用astor.to_source
将 AST 转换为 Python 代码。
tree = ast.parse("def foo():\n\tprint('Hello!')") print(astor.to_source(tree))
第二行的输出如下:
def foo(): print('Hello!')
通过这种方式,Python 脚本可以以编程方式生成 Python 代码。当您需要将文本从一种语言翻译成 Python 时,这会非常有用。