Python ast模块入门:怎么将代码字符串转换为抽象语法树

Miles
Miles
发布时间:2026-10-23 | 5人浏览过
原创
模块:Python内置 ast 核心函数:ast.parse() 适用场景:静态代码分析 / 语法校验 / 代码审计

在 Python 中,将一段代码字符串转换为抽象语法树(Abstract Syntax Tree,简称 AST)的标准方法是调用内置模块 ast 的 ast.parse() 函数。该函数接收包含 Python 代码的字符串作为输入,经过内部的词法切分与语法推导,返回以 ast.Module(或对应模式)为根节点的节点对象树。整个过程不需要执行代码,也不依赖外部第三方依赖,是进行代码静态检测、代码规范扫描、安全规则审计以及语法结构重塑的基础。

一、基础流程:从字符串到 AST 对象

调用 ast.parse() 只需要传入目标源码字符串。Python 解释器内置的解析器会分析文本中的语法结构,生成对应的树形节点。以下代码演示了最基础的转换与打印操作:

import ast

# 1. 准备待分析的 Python 源码字符串
source_code = "x = 10 + y"

# 2. 将字符串解析为抽象语法树根节点
tree = ast.parse(source_code)

# 3. 验证返回对象类型与内存结构
print("根节点类型:", type(tree))
print("直接打印对象:", tree)

运行上述代码后,终端输出如下所示:

根节点类型: <class 'ast.Module'>
直接打印对象: <ast.Module object at 0x...>

输出表明,ast.parse() 返回的是一个 ast.Module 对象。这个对象代表整个代码单元的顶层模块。如果直接对它执行 print(),只能得到对象的内存地址,无法直接肉眼观察语法树中的分支。为了查看这棵树的具体构成,可以使用 ast.dump() 进行格式化转储。

# 使用 ast.dump 输出直观结构(Python 3.9+ 支持 indent 参数)
print(ast.dump(tree, indent=4))

转储后的输出清晰展现出多层嵌套的语法节点:最外层是 Module,它的 body 列表包含一个赋值节点 Assign,赋值目标是变量名 x(上下文为 Store),被赋的值是一个双目运算节点 BinOp,由常数 10、加法操作符 Add 以及加载变量 y(上下文为 Load)共同组成。

Python ast模块入门:怎么将代码字符串转换为抽象语法树
图 1:代码字符串经由 ast.parse() 转换为 AST 对象的阶段拆解

二、节点解构:AST 树的分层结构与元数据

通过 ast.parse() 生成的并不是普通的字典或嵌套列表,而是一组由 ast.AST 基类派生出来的类实例。理解这些类及其属性,是后续编写代码检查工具的前提。

以语句 x = 10 + y 为例,其 AST 层级自顶向下展开如下:

  • ast.Module:根节点。它包含一个 body 属性,类型为 Python 列表,用来按顺序存放顶层的所有语句。
  • ast.Assign:赋值语句节点。它拥有两个核心属性:targets(被赋值的对象列表,支持链式赋值如 a = b = 1)和 value(等号右侧的值)。
  • ast.Name:标识符变量节点。通过 id 记录变量名称(如 'x' 和 'y'),并通过 ctx(上下文)指明当前标识符是正在被写入(ast.Store())还是正在被读取(ast.Load())。
  • ast.BinOp:双目运算节点。包含 left(左操作数)、op(操作符类型,如 ast.Add)以及 right(右操作数)。
  • ast.Constant:字面量常量节点。在 Python 3.8 及之后版本中,数字、字符串、布尔值及 None 均统一表示为 Constant,真实值通过 value 属性读取。

除了代码语法本身,每个具象节点还会携带源代码的位置元数据:

  • lineno:该语法元素在源代码中起始的行号(从 1 开始计)。
  • col_offset:该元素在起始行中的 UTF-8 字节/字符列偏移(从 0 开始计)。
  • end_lineno 与 end_col_offset:该元素结束的行号与列偏移(Python 3.8+ 完整支持)。
Python ast模块入门:怎么将代码字符串转换为抽象语法树
图 2:赋值语句在 AST 中的父子节点拆解及属性关系

三、核心参数解析:控制 ast.parse() 的解析模式

ast.parse() 的函数完整签名如下:

ast.parse(source, filename='<unknown>', mode='exec', *, type_comments=False, feature_version=None)

在实际使用中,最为关键的参数是 mode。该参数决定了解析器以何种语法规则处理输入的代码字符串:

模式 (mode) 生成根节点 允许的代码内容 典型应用场景
'exec' (默认) ast.Module 任意合法的 Python 模块代码(多语句、类/函数定义、控制流) 脚本文件扫描、静态代码规范分析、全局导入检查
'eval' ast.Expression 仅限单个表达式(不允许任何语句、赋值符号 = 或 import) 安全公式计算前置检查、条件表达式规则引擎
'single' ast.Interactive 单条交互式语句(如在终端 REPL 中输入的一行) 自定义命令行 REPL 环境、交互式执行器原型

如果希望限制用户只能输入纯数学或逻辑表达式(例如仅允许 a + b * 2,禁止执行 import os; os.system('rm -rf /')),必须将 mode 设置为 'eval'。一旦传入的内容包含语句或赋值,解析器会立即在语法层面阻断并抛出异常:

import ast

# 合法的 eval 模式解析
eval_tree = ast.parse("price * 0.85 + shipping", mode='eval')
print("eval 模式根节点:", type(eval_tree))  # 输出: <class 'ast.Expression'>

# 尝试在 eval 模式中解析赋值语句(会触发 SyntaxError)
try:
    ast.parse("x = 10", mode='eval')
except SyntaxError as e:
    print(f"解析被成功拦截: {e.msg} (行 {e.lineno}, 列 {e.offset})")

此外,filename 参数用于指定这段代码的来源标识。当后续发生语法报错或记录代码行位置时,报错信息中会展示该文件名;默认为 '<unknown>'。如果是在分析某个具体的磁盘文件,传入真实的文件路径能使异常跟踪和日志排查更为直观。

Python ast模块入门:怎么将代码字符串转换为抽象语法树
图 3:ast.parse() 三种工作模式的适用边界与节点类型对比

四、异常处理:捕获 SyntaxError 进行语法校验

如果传入的代码字符串包含不符合 Python 语法的字符、错误的缩进或未闭合的括号,ast.parse() 不会返回语法树,而是会立即抛出内置的 SyntaxError 异常。

通过捕获该异常,可以在不用真实执行代码的前提下,精准定位语法错误的具体位置与原因:

import ast

bad_code = """def calc_sum(a, b):
    return a + * b  # 故意构造的语法错误
"""

try:
    tree = ast.parse(bad_code, filename="test_script.py")
except SyntaxError as err:
    print("捕获到语法错误!")
    print(f"错误信息 (msg): {err.msg}")
    print(f"出错文件 (filename): {err.filename}")
    print(f"出错行号 (lineno): {err.lineno}")
    print(f"出错列号 (offset): {err.offset}")
    print(f"代码片段 (text): {repr(err.text)}")

执行后的捕获结果如下:

捕获到语法错误!
错误信息 (msg): invalid syntax
出错文件 (filename): test_script.py
出错行号 (lineno): 2
出错列号 (offset): 16
代码片段 (text): '    return a + * b  # 故意构造的语法错误\n'

借助这一机制,开发者只需数行代码便可封装一个轻量级的 Python 代码语法校验器,用于在用户提交代码、上传脚本或保存配置时进行前置验证。

五、树的后序处理:遍历与检查节点

成功获得 tree 对象后,通常需要遍历节点以提取所需信息。标准库 ast 提供了两种核心的遍历与检查方式:

1. 扁平化全局遍历:ast.walk()

ast.walk(tree) 是一个生成器函数,它会以未指定顺序递归产出树中的所有后代节点。当只需要快速统计某种类型的节点(例如查找代码中所有的函数调用、找出所有硬编码的常量),ast.walk() 非常简便:

import ast

code = """
import os
import sys

def run_task(name):
    print("Running:", name)
    timeout = 30
    return os.path.exists(name)
"""

tree = ast.parse(code)

# 提取代码中定义的所有函数名
func_names = [node.name for node in ast.walk(tree) if isinstance(node, ast.FunctionDef)]
print("定义的所有函数:", func_names)

# 提取代码中导入的所有模块名
imported_modules = []
for node in ast.walk(tree):
    if isinstance(node, ast.Import):
        for alias in node.names:
            imported_modules.append(alias.name)
    elif isinstance(node, ast.ImportFrom):
        imported_modules.append(node.module)

print("导入的所有模块:", imported_modules)

2. 访问者模式:ast.NodeVisitor

如果需要维护语法上下文(例如判断某个变量是否是在特定函数内部被赋值,或者按照严格的树形自顶向下顺序处理),应继承 ast.NodeVisitor 类。NodeVisitor 会根据节点的类名自动分发调用对应的 visit_ClassName 方法:

import ast

class SecurityAuditVisitor(ast.NodeVisitor):
    def __init__(self):
        self.sensitive_calls = []

    def visit_Call(self, node):
        # 检查函数调用名
        if isinstance(node.func, ast.Name):
            if node.func.id in ("eval", "exec", "__import__"):
                self.sensitive_calls.append((node.func.id, node.lineno))
        # 必须调用 generic_visit,确保子节点继续被遍历
        self.generic_visit(node)

code_snippet = """
a = 10
eval("a + 20")
print(a)
exec("b = 30")
"""

visitor = SecurityAuditVisitor()
visitor.visit(ast.parse(code_snippet))
print("审计发现的高危调用:", visitor.sensitive_calls)
Python ast模块入门:怎么将代码字符串转换为抽象语法树
图 4:AST 语法树产出后的三种核心处理方法与适用场景

六、注意事项与边界条件

重要边界提醒

1. 只解析语法,不执行也不推导类型:ast.parse() 只检验代码形式是否符合 Python 语法。如果代码中调用了不存在的函数、读取了未定义的变量或存在除以零等运行时逻辑错误,解析器均会正常通过并生成树。语法正确并不等于程序运行正确。

2. Python 版本兼容性:自 Python 3.8 起,传统的 ast.Num、ast.Str、ast.Bytes、ast.NameConstant 均被标记为弃用并整合为 ast.Constant。编写跨版本通用的 AST 分析脚本时,推荐优先判断 isinstance(node, ast.Constant)。

3. 递归深度限制:虽然 ast.parse() 本身高度安全且不执行系统调用,但当面对恶意构造的超长嵌套括号或表达式(例如上万层嵌套列表)时,底层解析器可能会触发栈溢出或 RecursionError。对外暴露解析接口时,应提前限制输入字符串的最大长度。

掌握 ast.parse() 是深入探索 Python 编译原理、编写自定义 Linter 以及实施自动化重构的核心第一步。通过合理选择 mode 参数、规范捕获 SyntaxError,再结合 ast.walk() 或 ast.NodeVisitor,开发者便能高效、安全地掌控 Python 源码的内部结构。

相关标签:

本站声明

本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。

Python 相关产品和应用

全部下载

专题列表