当前位置:首页>python>我花了三天手写解析器,同事告诉我 Python 自带了一个——ast 模块

我花了三天手写解析器,同事告诉我 Python 自带了一个——ast 模块

  • 2026-10-11 07:55:20
我花了三天手写解析器,同事告诉我 Python 自带了一个——ast 模块

我花了三天手写解析器,同事告诉我 Python 自带了一个——ast 模块

有一次我接到一个需求:把一个旧项目里所有 print() 调用改成 logger.info()。

文件有一百多个,手改是不可能的。我想写个脚本用正则替换,大概写了两个小时,越写越崩溃——print 出现在字符串里的怎么办?出现在注释里的怎么办?多行参数的怎么办?

最后我把半成品脚本删掉了,打算手改。

第二天同事来了,看了一眼问题,说:你用 ast 不就行了?

我当时完全不知道这个模块是什么。


ast 是什么

ast 是 Python 标准库里的抽象语法树(Abstract Syntax Tree)模块。

每一段 Python 代码,Python 解释器在运行之前都会把它解析成一棵树。ast 模块让你能够直接操作这棵树——读它、遍历它、修改它、再把它转回代码。

先感受一下:

import ast

code = """
x = 1
print(x + 2)
"""

tree = ast.parse(code)
print(ast.dump(tree, indent=2))

运行结果(部分):

Module(
  body=[
    Assign(
      targets=[Name(id='x', ctx=Store())],
      value=Constant(value=1)
    ),
    Expr(
      value=Call(
        func=Name(id='print', ctx=Load()),
        args=[
          BinOp(
            left=Name(id='x', ctx=Load()),
            op=Add(),
            right=Constant(value=2)
          )
        ],
        keywords=[]
      )
    )
  ]
)

每一行代码都被拆成了结构化的节点。print(x + 2) 是一个 Call,函数名是 Name(id='print'),参数是 BinOp(二元运算)。

再也不用猜正则会不会误匹配了。


遍历 ast:找出所有函数调用

ast.NodeVisitor 是一个遍历器基类,重写 visit_ 方法就能处理对应类型的节点:

import ast

code = """
def process():
    print("start")
    result = calculate(x, y)
    print(f"result: {result}")
    logger.info("done")
"""

class CallFinder(ast.NodeVisitor):
    def visit_Call(self, node):
        if isinstance(node.func, ast.Name):
            print(f"第 {node.lineno} 行: 调用了 {node.func.id}()")
        elif isinstance(node.func, ast.Attribute):
            print(f"第 {node.lineno} 行: 调用了 {node.func.attr}()")
        self.generic_visit(node)  # 继续遍历子节点

tree = ast.parse(code)
finder = CallFinder()
finder.visit(tree)

运行结果:

第 3 行: 调用了 print()
第 4 行: 调用了 calculate()
第 5 行: 调用了 print()
第 6 行: 调用了 info()

这个输出精准定位了每一次函数调用,行号准确,不会被注释或字符串干扰。


修改 ast:把 print 换成 logger.info

ast.NodeTransformer 是一个转换器基类,visit_ 返回新节点就完成替换:

import ast

class PrintToLogger(ast.NodeTransformer):
    def visit_Call(self, node):
        self.generic_visit(node)  # 先处理子节点
        
        # 判断是不是 print() 调用
        if isinstance(node.func, ast.Name) and node.func.id == 'print':
            # 构造 logger.info(...)
            new_func = ast.Attribute(
                value=ast.Name(id='logger', ctx=ast.Load()),
                attr='info',
                ctx=ast.Load()
            )
            node.func = new_func
        
        return node

code = """
print("start processing")
x = calculate(data)
print(f"result: {x}")
"""

tree = ast.parse(code)
new_tree = PrintToLogger().visit(tree)
ast.fix_missing_locations(new_tree)

# 转回代码字符串
import ast
print(ast.unparse(new_tree))

运行结果:

logger.info('start processing')
x = calculate(data)
logger.info(f'result: {x}')

精准替换,只动了 print,其他代码一字未改。

处理一百个文件的脚本我下午就写完了,两个小时都不到。


静态分析:找出代码里所有没用的变量

这是一个稍微进阶一点的用法——找出函数里赋值了但从没读过的变量:

import ast

class UnusedVarFinder(ast.NodeVisitor):
    def __init__(self):
        self.assigned = {}  # 变量名 -> 赋值节点行号
        self.used = set()   # 被读过的变量名

    def visit_Assign(self, node):
        for target in node.targets:
            if isinstance(target, ast.Name):
                self.assigned[target.id] = node.lineno
        self.generic_visit(node)

    def visit_Name(self, node):
        if isinstance(node.ctx, ast.Load):
            self.used.add(node.id)
        self.generic_visit(node)

    def report(self):
        unused = {name: line for name, line in self.assigned.items()
                  if name not in self.used}
        for name, line in unused.items():
            print(f"第 {line} 行: 变量 '{name}' 赋值后从未使用")

code = """
def process(data):
    result = transform(data)
    temp = result * 2   # temp 赋值但没用
    output = format(result)
    return output
"""

tree = ast.parse(code)
finder = UnusedVarFinder()
finder.visit(tree)
finder.report()

运行结果:

第 4 行: 变量 'temp' 赋值后从未使用

pylint、flake8 背后的原理就是这个,只是做得更全面更精确。


什么时候值得用 ast

不是所有字符串处理都要上 ast,但有几类场景用了之后会后悔没早用:

  • 批量修改代码风格(函数调用替换、导入语句整理)
  • 代码合规检查(禁止用某些函数、找出潜在危险操作)
  • 自动生成代码(从类定义自动生成序列化代码)
  • 分析项目依赖(找出哪些模块被哪些文件 import)

以前我碰到这类需求,第一反应是正则。现在第一反应是 ast。

正则处理的是字符串,ast 处理的是语义。这是两个不同维度的东西,不是同一个工具在不同场景的选择,是完全不同的抽象层次。


那次 print 替换之后,我在项目里又用 ast 写了一个代码审查工具,专门检查有没有直接用 open() 而不用 with 的写法,跑一遍全扫出来,省了不少 review 时间。

学 Python 这几年,每隔一段时间都会发现标准库里有个自己从来没用过但绝了的模块。ast 是让我最意外的一个。

你用过 ast 或者类似的代码分析工具吗?欢迎评论区聊聊,说不定我也没想到的用法。

最后,别忘了关注「有为大青年」,我们下期见~

最新文章

随机文章