65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 AST 包分析 Python

emptyStarIconemptyStarIconemptyStarIconemptyStarIconemptyStarIcon

0/5 (0投票)

2021年8月22日

CPOL

9分钟阅读

viewsIcon

17012

本文介绍了如何使用开源 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.dumptree 转换为字符串。

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 没有子节点,所以它们是叶子节点。其他节点是分支节点。

图 1:简单的树层级结构

树中的每个节点都有一个深度值,用于标识它与根节点之间的连接数。在此示例中,节点 E 的深度为 0,节点 C 的深度为 1,节点 G 的深度为 2。

3.2 抽象语法树 (ASTs)

我在小学时,需要使用句子树来分析句子。根节点代表整个句子,每个根节点有两个子节点:一个代表主语,一个代表谓语。在简单的句子中,主语由名词短语表示,谓语由动词短语表示。图 2 展示了句子:“This sentence is simple”的树。

图 2:示例句子树

在这棵树中,叶子节点包含构成文本的单个字符串。分支节点标识了每个叶子节点的用途以及它在句子中所扮演的角色。

如果你能理解句子树如何表示英语句子,那么理解抽象语法树如何表示 Python 代码就不会有任何困难。当 ast.parse 分析 Python 代码时,根节点有四种形式之一:

  • module - 语句集合
  • function - 函数定义
  • interactive - 交互式会话中的语句集合
  • expression - 简单表达式

图 3 说明了前面演示的简单 Python for 循环的 AST。根节点是 module。

图 3:示例 Python AST

几乎我遇到的每个 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 说明了其顶级节点。

图 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=[])

这个 module 包含一个语句,该语句是一个表达式。该表达式包含两个二元运算:加法和乘法。变量 x 由一个 name 节点标识,两个数值由 value 节点标识。

3.2 AST 类

Python AST 中的每种节点类型在 ast 包中都有一个相应的类。Module 由 Module 类的实例表示,表达式由 Expr 实例表示。函数定义由 FunctionDef 实例表示,类定义由 ClassDef 实例表示。

一个节点的每个子节点对应于相应类的一个属性。在图 4 中,类定义节点有名为 name、body、bases、keywords 和 decorator list 的子节点。为了存储这些信息,ClassDef 类有名为 namebodybaseskeywordsdecorator_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 列出了其中的六个函数,并提供了每个函数的描述。

表 1:astor 包的函数
函数 描述

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 时,这会非常有帮助。

5. 历史记录

  • 2021年8月22日:首次发布
  • 2021年8月24日:添加了 ASTOR 链接
© . All rights reserved.