使用 AST 包分析 Python





0/5 (0投票)
本文介绍了如何使用开源 ast 包来分析 Python 代码。
1. 引言
正如 StackOverflow 所显示的那样,Python 近年来的人气急剧上升。因此,需要更多的软件工具来读取和分析 Python 代码。开源的 ast 包为此提供了许多功能,本文的目的是介绍它的特性。
AST 代表抽象语法树,稍后的部分将解释这些树是什么以及为什么它们很重要。ast 包可以读取 Python 代码到 AST 中,AST 的每个节点都由一个不同的 Python 类表示。
本文的最后部分讨论了 astor (AST observe/rewrite) 包。它提供了读取和写入 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. 抽象语法树 (ASTs)
为了理解解析器的结果,理解抽象语法树 (ASTs) 很重要。这些树体现了文档内容的结构,无论它是用 Python 这样的编程语言还是像英语这样的自然语言编写的。
本节将解释 AST 是什么,然后介绍表示 AST 节点的类。但在介绍抽象语法树之前,我想先退一步,解释一下树结构是什么。
3.1 树结构
当数据元素以一个单一元素为起点形成一个层级结构时,这些元素及其关系就可以表示为一棵树。常见的树包括组织结构图、文件导航器和家谱。树结构在软件开发中很常见,尤其是在网络、图形和文本分析领域。
在处理树时,开发人员依赖于一套通用术语:
- 树中的每个元素称为一个节点。
- 最顶层的元素称为根节点。
- 如果一个节点连接着其下方的节点,则第一个节点称为父节点,连接的节点是父节点的子节点。
- 除了根节点外,每个节点都有一个父节点。有一个或多个子节点的节点是分支节点,没有子节点的节点称为叶子节点。
图 1 展示了一个简单的树。节点 E 是根节点,节点 B、C 和 D 是它的子节点。节点 A 和 F 是 B 的子节点,节点 G 是 D 的子节点。节点 A、F、C 和 G 没有子节点,所以它们是叶子节点。其他节点是分支节点。
树中的每个节点都有一个深度值,用于标识它与根节点之间的连接数。在此示例中,节点 E 的深度为 0,节点 C 的深度为 1,节点 G 的深度为 2。
3.2 抽象语法树 (ASTs)
我在小学时,需要使用句子树来分析句子。根节点代表整个句子,每个根节点有两个子节点:一个代表主语,一个代表谓语。在简单的句子中,主语由名词短语表示,谓语由动词短语表示。图 2 展示了句子:“This sentence is simple”的树。
在这棵树中,叶子节点包含构成文本的单个字符串。分支节点标识了每个叶子节点的用途以及它在句子中所扮演的角色。
如果你能理解句子树如何表示英语句子,那么理解抽象语法树如何表示 Python 代码就不会有任何困难。当 ast.parse
分析 Python 代码时,根节点有四种形式之一:
- module - 语句集合
- function - 函数定义
- interactive - 交互式会话中的语句集合
- expression - 简单表达式
图 3 说明了前面演示的简单 Python for
循环的 AST。根节点是 module。
几乎我遇到的每个 Python AST 都以 module 作为其根节点。module 由一个或多个语句组成,大多数类型的语句又由一个或多个表达式组成。接下来的讨论将探讨语句和表达式的主题。
3.2.1 语句
在前面的 AST 中,module 包含一个表示 for
循环的单个语句。除了 for
循环之外,AST 语句还可以表示函数定义、类定义、while
循环、if
语句、return
语句和 import
语句。
每个语句节点有一个或多个子节点,其子节点的数量和类型取决于语句的类型。例如,函数定义至少有四个子节点:一个标识符、参数、一个装饰器列表和一个构成其主体的语句集。为了说明这一点,以下代码解析了定义名为 foo
的函数的 Python 代码。
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=[])
从左到右看,可以清楚地看出根节点是 module,它的子节点是函数定义。函数定义有四个子节点,代表主体的子节点有一个子节点,因为函数的主体包含一行代码。
类定义尤其重要,每个类定义有五个子节点:名称、零个或多个基类、零个或多个关键字、零个或多个语句以及零个或多个装饰器。类中的每个方法都表示为一个函数定义语句。
为了说明这一点,请考虑以下简单的类定义:
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)")
不打印整个 AST,图 4 说明了其顶级节点。
许多语句,如 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=[])
这个 module 包含一个语句,该语句是一个表达式。该表达式包含两个二元运算:加法和乘法。变量 x 由一个 name 节点标识,两个数值由 value 节点标识。
3.2 AST 类
Python AST 中的每种节点类型在 ast 包中都有一个相应的类。Module 由 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,你可以使用命令 pip install astor
来安装 astor。截至本文撰写之时,当前版本是 0.8.1。
astor 提供了一些有用的类和函数,可以简化与 Python AST 的工作。表 1 列出了其中的六个函数,并提供了每个函数的描述。
函数 | 描述 |
---|---|
| 将 AST 转换为 Python 代码 |
code_to_ast(codeobj) | 重新编译模块为 AST 并 提取函数的子 AST |
parse_file(file) | 将 Python 文件解析为 AST |
| 使用缩进漂亮地打印 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 时,这会非常有帮助。
5. 历史记录
- 2021年8月22日:首次发布
- 2021年8月24日:添加了 ASTOR 链接