深入解析Python中的AST模块:代码分析与修改的强大工具

ast(抽象语法树)是Python中非常有用的模块。我们可以通过分析python的抽象语法树来分析和修改python代码。

深入解析Python中的AST模块:代码分析与修改的强大工具

ast 在 python 代码语法解析之后、编译为字节码之前使用。

1. ast 1. 获取语法树

ast模块的基本使用非常简单。我们可以通过以下代码快速得到一棵抽象语法树:

import ast
root_node = ast.parse("print 'hello world'")
root_node -> <_ast.Module object at 0x9e3df6c>

通过ast的parse方法得到ast树的根节点root_node。我觉得可以通过根节点遍历语法树来分析和修改python代码。

ast.parse(可以直接查看ast模块的源码)方法实际上调用了内置函数compile进行编译,如下所示:

def parse(source, filename='', mode='exec'):
    """
    Parse the source into an AST node.
    Equivalent to compile(source, filename, mode, PyCF_ONLY_AST).
    """
    return compile(source, filename, mode, PyCF_ONLY_AST)

传递特殊标志 = PyCF_ONLY_AST 进行编译,通过编译返回抽象语法树。

(有时间可以深入研究compile的第四个参数flag)

2. 节点类型分析

语法树中的每个节点对应ast下的一个类型。根节点的类型为ast.Moudle。分析时,通过isinstance函数可以轻松确定节点类型。

ast中存在的所有节点类型可以参考:ast节点类型

例如a=10这样的语句对应的是ast.Assign节点类型,Assign节点类型有两个子节点,分别是ast.Name类型的a和ast.Num类型的10。

我们可以使用ast.dump(node)函数格式化节点并打印来查看节点内容。以代码行“a = 10”为例。

Module(body=[Assign(targets=[Name(id='a', ctx=Store())], value=Num(n=10))])

(1)根节点

模块(body=[分配(目标=[名称(id='a', ctx=Store())], 值=Num(n=10))])

根节点是Module类型。由于只有一行代码,所以所有根节点只有一个子节点Assign。

(2)子节点

分配(目标= [名称(id ='a',ctx = Store())],值= Num(n = 10))

上面的Assign节点有3个子节点,分别是Name、Store和Num。

名称(id='a', ctx=Store())

数量(n=10)

Name 有一个子节点 Store。

Store()(Store表示Name中操作时赋值的值,类型有Load和del,具体请参考节点类型的文档)

一行简单的代码如“a = 10”,我们就可以通过上面的ast树来分析并修改代码结构。

2. 语法树遍历分析

可以通过ast模块提供的visitor来遍历语法树。

ast.NodeVisitor是一个专门用来遍历语法树的工具。我们可以继承这个类来完成语法树的遍历以及遍历过程中的处理。

1. 访客的定义

class CodeVisitor(ast.NodeVisitor):
    def generic_visit(self, node):
        print type(node).__name__
        ast.NodeVisitor.generic_visit(self, node)
 
    def visit_FunctionDef(self, node):
        print type(node).__name__
        ast.NodeVisitor.generic_visit(self, node)
 
    def visit_Assign(self, node):
        print type(node).__name__

如上面的代码所示,定义了类CodeVisitor,继承自NodeVisitor。函数主要有两种类型,一种是generic_visit,另一种是“visit_”+“Node type”。

访问者首先从根节点root开始遍历。遍历过程中,假设节点类型为Assign。如果存在visit_Assign类型的函数,则调用visit_Assgin函数。如果不存在,则调用 generic_visit 函数。

一般来说,每种节点类型都有专用的类型处理函数。如果不存在,则调用通用处理函数generic_visit。

关于visitor的语法树遍历,stackoverflow上有一篇文章讲得比较详细:Simple example of how to use ast.NodeVisitor

注意:

在每个函数处理中,根据需要添加代码ast.NodeVisitor.generic_visit(self, node),否则访问者将不会继续访问当前节点的子节点。

例如,如果您定义以下函数:

def Visit_Moudle(自身, 节点):

打印类型(节点).__name__

然后,首先访问根节点root,它是Moudle类型,会调用visit_Moudle函数。由于visit_Moudle函数中没有调用NodeVisitor.generic_visit(self, node),因此本次遍历只遍历了根节点root,并没有遍历其他节点。

2.步行模式遍历

还可以通过ast.walk遍历ast树,如下:

for node in ast.walk(tree):
    if isinstance(node, ast.FunctionDef):
        print(node.name)

3、节点修改

ast模块还提供了NodeTransfomer节点来支持节点的修改。 NodeTransfomer继承自NodeVisitor,并重写了generic_visit函数。

对于NodeTransfomer的generic_visit和visit_+节点类型函数,都需要返回一个节点。您可以返回原始节点、新的替换节点或返回 Node 来表示删除该节点。

假设我们有以下代码:

"""ast test code"""
a = 10
b = "test"
print a

我们定义一个 NodeTransform 访问者如下:

class ReWriteName(ast.NodeTransformer):
    def generic_visit(self, node):
        has_lineno = getattr(node, "lineno", "None")
        col_offset = getattr(node, "col_offset", "None")
        print type(node).__name__, has_lineno, col_offset
        ast.NodeTransformer.generic_visit(self, node)
        return node
 
    def visit_Name(self, node):
        new_node = node
        if node.id == "a":
            new_node = ast.Name(id = "a_rep", ctx = node.ctx)
        return new_node
 
    def visit_Num(self, node):
        if node.n == 10:
            node.n = 100
        return node

在visit_Name 中,将变量“a”替换为变量“a_rep”。当执行a = 10并打印a时,a将被a_rep替换并返回一个新节点。

在visit_Num中,只需将10替换为100并返回修改后的原始节点。

我们通过以下方式使用这个 NodeTransfomer 访问者:

file = open("code.py", "r")
source = file.read()
visitor = ReWriteName()
root = ast.parse(source)
root = visitor.visit(root)
ast.fix_missing_locations(root)
 
code_object = compile(root, "", "exec")
exec code_object

ast 在 python 解析语法之后、将其编译成 pyCodeObject 字节码结构之前使用。经过NodeTransformer修改后,返回修改后的语法树。我们通过内置模块compile将其编译成pyCodeObject对象,交给python虚拟机执行。

执行结果:100

可以看到,我们把a=10同时打印a,将a名称改为a_rep,并将10替换为100,最终打印结果为100,语法树的节点修改成功。

关于节点的修改,这里有一些很好的例子可供参考:

注意:

修改语法树节点时要小心,尤其是删除语法树节点时,因为修改或删除后可能会返回错误的语法树,直到编译或执行时才会发现问题。

通过节点修改python代码可以通过上面的方法完成,但是请注意,使用visitor的代码中有一行代码如ast.fix_missing_locations(root)。这是因为我们创建的节点不包含lineno和col_offset。必须手动修改属性,添加新添加节点代码的指定、行位置和偏移位置。

4.修复节点位置

1 属性分析

每个节点都有一些相应的属性。 lineno 和 col_offset 是每个节点必须具有的属性,分别表示行号和在该行中的偏移量。

另外,每个节点都有一些自己特殊的属性,比如调用的Module包含body属性,Assign包含targets属性等。

lineno和col_offset这两个属性,如果是python中原代码的节点,如Assign、Name、Num等(注:Moudle、Store等节点没有lineno和col_offset属性),但是如果我们添加它们通过 NodeTransFormer 节点,这些属性默认不存在。我们可以通过三种方法修复这些节点的lineno和col_offset属性。

2.属性修复

我们可以使用相应的方法来修复默认没有lineno和col_offset的节点的位置,以方便在代码中获取每个节点的位置信息。修复方法主要有三种。

1)ast.fix_missing_locations(节点)

该函数递归地将父节点的位置信息(lineno和col_offset)分配给没有位置信息的子节点。

2) ast.copy_location(new_node, 节点)

将节点的位置信息复制到new_node节点并返回new_node节点。当我们用新节点替换旧节点时,这种方法更适合。

3)ast.increment_lineno(节点,n=1)

该节点node及其所有子节点的行号加n。

3 分析

我们通过“3.节点修改”​​中的例子来分析位置信息。

在示例中,我们仅在访问期间返回新节点_Name。此时,该节点不具有lineno和col_offset属性。我们可以通过两种方式获得它们。

首先如上面的代码,使用ast.fix_missing_locations函数来修复。在“a = 10”和“print a”中,Name节点a与父节点的lineno相同,但此时col_offset会不同。

其次,我们修改visit_Name的代码如下:

def visit_Name(self, node):
    new_node = node
    if node.id == "a":
        new_node = ast.Name(id = "a_rep", ctx = node.ctx)
        ast.copy_location(new_node, node)
    return new_node

通过copy_location将旧节点的位置信息复制到新节点。

参考:

Green Tree Snakes - 缺失的 Python AST 文档 - 非常详细的 ast 模块分析文档。

如何使用ast.NodeVisitor的简单示例——stackoverflow上一篇文章中比较清晰的答案

检测 AST——简单分析和应用

ast — 抽象语法树--官方文档

--更详细地介绍了修改内容

你可能想看:
« 上一篇
下一篇 »

相关推荐

python爬虫教程PDF,Python爬虫书PDF

2026年09月02日

31阅读

外国教程python,python编程教学视频

2026年09月02日

25阅读

python简明教程沈洁元,python详细基础教程

2026年09月02日

21阅读

python库包教程,python 包

2026年09月02日

27阅读

python教程大全集,python教程600集

2026年09月02日

26阅读

python入门教程完整版,python零基础入门全套教程视频

2026年09月02日

22阅读