一、抽象语法树
1、什么是抽象语法树
在计算机科学中,抽象语法树(abstract syntax tree
,AST
),是源代码的抽象语法结构的树状表现形式,这里特指编程语言的源代码。AST是编译器或解释器在处理源代码时所使用的一种中间表示形式,在编译和代码生成过程中起着关键作用。
之所以说语法是「抽象」的,是因为这里的语法并不会表示出真实语法中出现的每个细节。
AST中的每个节点表示源代码中的一个语法构造,如变量声明、表达式、函数调用、控制结构等。树的根节点通常表示整个源代码文件,而子节点表示具体的语法元素及其关系。例如,一个函数声明的AST节点可能包含多个子节点,如函数名、参数列表和函数体等。
2、使用场景
python 和 Java 类似,解释器实际上分为两部分:编译器和虚拟机,先将代码编译成字节码,然后再由虚拟机执行。
在编译时,主要用途包括:
- 语法检查:编译器可以通过遍历AST来检查源代码中是否存在语法错误。
- 语义分析:编译器可以使用AST来分析源代码的语义,例如识别类型错误、作用域错误等。
- 优化:编译器可以通过对AST进行变换和简化,实现源代码的优化。
- 代码生成:编译器可以根据AST生成目标代码,例如汇编语言或机器代码。
在实际应用中,AST除了用于编译器或解释器,还被用于诸如代码重构、静态分析和代码格式化等工具和技术中。
编译器和解释器的核心:AST是编译器和解释器处理源代码的关键数据结构。在对源代码进行语法分析之后,编译器或解释器会生成AST。接下来,它们可以在AST上进行进一步的分析、优化和代码生成。这使得编译器和解释器能够更高效地处理源代码,为生成可执行程序或执行脚本提供基础。
代码分析:AST在静态代码分析和静态类型检查中起着重要作用。通过分析AST,我们可以检测代码中的潜在错误、不良实践和安全漏洞,从而提高代码质量。
代码转换和优化:编译器、解释器和其他工具(如Babel或Webpack)可以使用AST来进行代码转换和优化。这些工具可以在AST上执行各种操作,如语法转换(例如将ES6+语法转换为ES5语法)、代码压缩、代码拆分和常量传播等。这有助于提高程序的性能和兼容性。
代码生成:基于AST,编译器和解释器可以生成目标代码(例如机器代码、字节码或其他编程语言的代码)。这使得跨平台编译和运行成为可能,例如:将C++代码编译为WebAssembly,以便在Web浏览器中运行。
代码重构和编辑器支持:AST在代码重构和编辑器支持中也起着重要作用。通过分析和操作AST,我们可以实现自动化的代码重构、代码补全、语法高亮、错误检查等功能,从而提高开发者的生产力。
3、AST还能做什么
抽象语法树(AST)在编程语言处理、软件工程和开发工具中发挥着关键作用。除了前面讨论过的用途之外,AST还可以用于以下方面:
代码生成器:可以根据AST生成代码模板和脚手架工具。例如,根据类和方法定义生成REST API的客户端和服务器端实现。
语言转换:通过分析源语言的AST,然后将其转换为目标语言的AST,可以实现源代码到目标代码的转换。例如,将TypeScript代码转换为JavaScript代码。
代码覆盖率分析:通过分析AST,我们可以检测测试用例覆盖的代码范围,从而衡量测试质量和查找潜在的漏洞。
文档生成:AST可以用于提取源代码中的注释、类、方法和属性等信息,从而自动生成API文档。
代码安全性分析:通过分析AST,可以识别不安全的代码模式和潜在的安全漏洞,从而提高软件安全性。
代码审查:AST可以帮助自动化检查代码是否符合团队的编程规范和约定,从而提高代码质量和一致性。
依赖关系分析:通过分析AST,可以识别源代码中的模块、类和函数之间的依赖关系,从而理解代码结构和优化代码组织。
自动补全和代码导航:通过分析AST,集成开发环境(IDE)和代码编辑器可以提供自动补全、代码导航、变量重命名等智能功能,从而提高开发者的生产力。
语言扩展和领域特定语言(DSL):AST可以用于设计和实现领域特定语言,这些语言可以更简洁地表示特定领域的问题和解决方案。例如,通过将DSL转换为目标编程语言的AST,可以生成可执行代码。
三、Python AST
Python AST是Python源代码的抽象语法树表示形式。它是Python编译器在解析源代码时生成的一种数据结构,用于表示源代码的语法结构。
Python官方提供的CPython解释器对python源码的处理过程如下:
- Parse source code into a parse tree (Parser/pgen.c)
- Transform parse tree into an Abstract Syntax Tree (Python/ast.c)
- Transform AST into a Control Flow Graph (Python/compile.c)
- Emit bytecode based on the Control Flow Graph (Python/compile.c)
即实际python代码的处理过程如下:
- 源代码解析 --> 语法树 --> 抽象语法树(AST) --> 控制流程图 --> 字节码
1、Python AST的基本结构
Python AST是由一系列节点(Node)组成的树形结构。每个节点代表了Python源代码中的一个语法结构,例如函数、类、变量、表达式等。每个节点都有一个类型(Type)和一些属性(Attribute),用于描述节点的语法结构和语义信息。
节点可以分类为:
- 常量节点(Literals)
- 变量节点(Variables)
- 表达式节点(Expressions)
- 声明节点(Statements)
- 控制流节点(Control flow,if/for/while等)
- 函数和类的定义节点(Function and class definitions)
- 异步和等待节点(Async and await)
- 顶层节点(Top level nodes)
以下是Python AST的一些常见节点类型:
Module
: 代表整个Python模块。FunctionDef
: 代表函数定义。ClassDef
: 代表类定义。Name
: 代表变量名。Constant
: 代表常量。BinOp
: 代表二元操作符表达式。
其余的节点类型见:ast --- 抽象语法树 — Python 3.12.1 文档
2、AST模块的基本用法
在Python中,我们可以使用ast模块提供的parse()函数将源代码解析为抽象语法树对象,并使用walk()方法遍历该对象。
除了ast.dump,有很多dump ast的第三方库,如astunparse, codegen, unparse等。这些第三方库不仅能够以更好的方式展示出ast结构,还能够将ast反向导出python source代码。
import ast
import astpretty
# 解析源代码,获得抽象语法树对象
ast_tree = ast.parse("print('Hello, world!')")
# 遍历抽象语法树对象
for node in ast.walk(ast_tree):
# print(type(node).__name__)
print("*"*66)
astpretty.pprint(node)
以上代码输出内容如下:
Module(
body=[
Expr(
lineno=1,
col_offset=0,
end_lineno=1,
end_col_offset=22,
value=Call(
lineno=1,
col_offset=0,
end_lineno=1,
end_col_offset=22,
func=Name(lineno=1, col_offset=0, end_lineno=1, end_col_offset=5, id='print', ctx=Load()),
args=[Constant(lineno=1, col_offset=6, end_lineno=1, end_col_offset=21, value='Hello, world!', kind=None)],
keywords=[],
),
),
],
type_ignores=[],
)
******************************************************************
Module(
body=[
Expr(
lineno=1,
col_offset=0,
end_lineno=1,
end_col_offset=22,
value=Call(
lineno=1,
col_offset=0,
end_lineno=1,
end_col_offset=22,
func=Name(lineno=1, col_offset=0, end_lineno=1, end_col_offset=5, id='print', ctx=Load()),
args=[Constant(lineno=1, col_offset=6, end_lineno=1, end_col_offset=21, value='Hello, world!', kind=None)],
keywords=[],
),
),
],
type_ignores=[],
)
******************************************************************
Expr(
lineno=1,
col_offset=0,
end_lineno=1,
end_col_offset=22,
value=Call(
lineno=1,
col_offset=0,
end_lineno=1,
end_col_offset=22,
func=Name(lineno=1, col_offset=0, end_lineno=1, end_col_offset=5, id='print', ctx=Load()),
args=[Constant(lineno=1, col_offset=6, end_lineno=1, end_col_offset=21, value='Hello, world!', kind=None)],
keywords=[],
),
)
******************************************************************
Call(
lineno=1,
col_offset=0,
end_lineno=1,
end_col_offset=22,
func=Name(lineno=1, col_offset=0, end_lineno=1, end_col_offset=5, id='print', ctx=Load()),
args=[Constant(lineno=1, col_offset=6, end_lineno=1, end_col_offset=21, value='Hello, world!', kind=None)],
keywords=[],
)
******************************************************************
Name(lineno=1, col_offset=0, end_lineno=1, end_col_offset=5, id='print', ctx=Load())
******************************************************************
Constant(lineno=1, col_offset=6, end_lineno=1, end_col_offset=21, value='Hello, world!', kind=None)
******************************************************************
Load()
以上内容是抽象语法树中的6个节点,其中,Module表示整个模块,Expr表示一个表达式,Call表示一个函数调用。那么我们可以画出来:
import ast
import ctree
ast_tree = ast.parse("print('Hello, world!')")
ctree.ipython_show_ast(ast_tree)
从语法树中可以看出,该语句加载(Load())了名(Name())为print的函数接口(func),函数传参(args)是值为’Hello, world!’(value)的常量(Constant)。
C语言分析:
import ast
import ctree
from ctree.transformations import PyBasicConversions
ast_tree = ast.parse("print('Hello, world!')")
# ctree.ipython_show_ast(ast_tree)
t = PyBasicConversions()
tree2 = t.visit(ast_tree)
ctree.ipython_show_ast(tree2)
3、使用AST模块进行代码分析
由于AST可以表示源代码的结构,因此,我们可以结合AST模块对Python代码进行语法分析、代码检查、重构等操作。
3.1. 语法分析
我们可以使用AST模块提供的NodeVisitor类来遍历代码并进行分析。NodeVisitor是一个抽象基类,它定义了许多方法,我们只需要重写需要的方法即可。
import ast
class MyVisitor(ast.NodeVisitor):
def visit_BinOp(self, node):
print(type(node).__name__)
# 解析源代码,获得抽象语法树对象
tree = ast.parse("a + b")
# 实例化并遍历分析器
visitor = MyVisitor()
visitor.visit(tree)
以上代码定义了一个分析器类MyVisitor,它继承自NodeVisitor并重写了visit_BinOp方法。visit_BinOp方法指在遍历二元运算符节点时被调用,因此可以在该方法中进行具体的分析。
3.2. 代码检查
AST模块提供了许多内置函数和类,可以用于检查代码是否符合某些规范。
import ast
# 检查函数名是否符合规范
def check_func_name(node):
func_name = node.name
if not func_name.startswith("test_"):
print(f"函数名{func_name}不符合规范!")
# 解析源代码,获得抽象语法树对象
tree = ast.parse("def test_add(a, b):\n return a + b\n\ndef sub(a, b):\n return a - b")
# 遍历抽象语法树对象
for node in ast.walk(tree):
if isinstance(node, ast.FunctionDef):
check_func_name(node)
以上代码定义了一个检查器函数check_func_name,它判断函数名是否以"test_"开头。在遍历抽象语法树时,如果找到了函数节点,就可以调用check_func_name进行检查。
3.3. 代码重构
代码重构是指对已有代码的结构和实现进行修改以改进代码的质量和维护性。AST模块可以帮助我们对Python代码进行重构,例如,将代码中的print语句替换为logging模块。
import ast
# 重构代码,将所有的print语句替换为调用logging模块
class MyTransformer(ast.NodeTransformer):
def visit_Print(self, node):
print_node = ast.Name(id="logging.info", ctx=ast.Load())
args_node = [ast.Str(s="Print statement at line {0}".format(node.lineno))]
args_node += node.values
return ast.Expr(value=ast.Call(func=print_node, args=args_node, keywords=[]))
# 解析源代码,获得抽象语法树对象
tree = ast.parse("print('Hello, world!')\n")
# 将抽象语法树对象重构为新的语法树对象
my_transformer = MyTransformer()
new_tree = my_transformer.visit(tree)
# 使用astunparse将语法树源代码还原为字符串
import astunparse
print(astunparse.unparse(new_tree))
以上代码定义了一个重构器类MyTransformer,它继承自NodeTransformer并重写了visit_Print方法。visit_Print方法指在遍历print语句节点时被调用,可以在该方法中进行替换操作。
参考:
你知道什么是AST语法树嘛?你真的了解AST语法树嘛?读到最后你将对AST语法树有新的认识! - 知乎
ast --- 抽象语法树 — Python 3.12.1 文档
Python Ast抽象语法树的介绍及应用详解 - Python技术站
https://www.cnblogs.com/qiulinzhang/p/14258626.html
Python中ast模块是什么?_笔记大全_设计学院
【python】ast模块介绍和使用_import ast-CSDN博客
不要再手动批量替换了,使用python AST模块批量替换_Python_阿呆_InfoQ写作社区
https://www.cnblogs.com/yssjun/p/10069199.html
python语法树可视化_mob649e815b1a71的技术博客_51CTO博客
Python中ast模块是什么?_笔记大全_设计学院
https://www.cnblogs.com/qiulinzhang/p/14258626.html
python 可视化AST、CFG学习_python显示ast树形状-CSDN博客
【python】ast模块介绍和使用_import ast-CSDN博客
Using IPython for AST Visualization — ctree alpha documentation