使用 AST 包分析 Python

本文介绍如何使用开源 ast 包分析 Python 代码
开源 ast 包提供了丰富的 Python 代码解析、分析和生成功能。本文讨论了这些功能并演示了如何在代码中使用它们。它还介绍了 astor 包的功能。

AI吧Python

一、简介

正如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该类具有名为namebodybaseskeywords和的属性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 时,这会非常有用。

发表评论

邮箱地址不会被公开。 必填项已用*标注