毕设,学一些ast的操作
python的编译过程
在python2.4及以前,从源代码到字节码的编译过程是这样的:
- 将源代码解析成解析树(parse tree)
- 从解析树出发,生成字节码
这并不是一个标准的编译器工作方式。因此从python2.5开始,上述过程的步骤变成了这样:
- 将源代码解析为解析树(Parser/pgen.c)
- 将解析树转换为抽象语法树(Python/ast.c)
- 将AST转换为控制流图(Python/compile.c)
- 基于控制流图(Python/compile.c)发出字节码
而我们现在所重点关注的便是抽象语法树的部分
相关配置与版本控制
对于编写ast相关的程序来说我所建议的python版本是python3.6,因为我们使用时不仅需要ast模块,也需要第三方库astunparse、ast2json等,从该第三方库的github Tags时间点进行推断,其活跃时间大概是18 19年,而此时的主流python版本大概是3.6.5
并且python在3.8版本开始对ast模块中做出了部分调整,导致了astunparse模块在3.8版本可能不可用的问题
比较重要的几处改动:
- 在 3.8 版更改: 类
ast.Constant
现在用于所有常量。 - 3.8 版后已移除: 旧的类
ast.Num
、ast.Str
、ast.Bytes
、ast.NameConstant
和ast.Ellipsis
仍然有效,但是它们会在未来的 Python 版本中被移除。同时,实例化它们会返回一个不同类的实例。 - 在
class ast.NodeVisitor
处,3.8 版后已移除:visit_Num()
,visit_Str()
,visit_Bytes()
,visit_NameConstant()
和visit_Ellipsis()
等方法现在已被弃用且在未来的 Python 版本中将不会再被调用。 请添加visit_Constant()
方法来处理所有常量节点。
总的来说是围绕着对常量类的统一来进行的,因此如果按照3.6中的处理方式去解析或反解析AST模块,很可能在3.8版本中是行不通的
python多版本控制
对于在一台电脑上控制两个python版本来说,最主要的问题在于如何区分调用两个版本,以及如何在两个版本中分别使用包管理工具pip
对于前者,在环境变量中加入两个python主程序的目录并分别命名即可,例如我对3.8版本主程序命名为python.exe,而对3.6版本则命名为python36.exe
对于后者,因为pip本身也属于python包,因此也可以使用python的-m参数来进行调用,因此按照如下模式执行即可
//python3.8
python -m pip [command of pip]
//python3.6
python36 -m pip [command of pip]
AST基础
ast模块对于编程中使用到的每个不同类型变量都有一个不同名称的类定义,从类定义到函数到运算符,应有尽有
下面来看几个重要的类定义:
ast.Expr()
ast.Expr(value)
当一个表达式,例如函数调用,本身作为一个语句出现并且其返回值未被使用或存储时,它会被包装在此容器中。value
保存本节中的其他节点之一,一个Constant
,Name
,Lambda
,Yield
或者YieldFrom
节点
ast.Call()
ast.Call(func, args, keywords, starargs, kwargs)
一个函数调用。 func
是函数,它通常是一个 Name
或 Attribute
对象。 对于其参数:
args
保存由按位置传入的参数组成的列表。keywords
保存了一个代表以关键字传入的参数的keyword
对象的列表。
当创建一个 Call
节点时,需要有 args
和 keywords
,但它们可以为空列表。 starargs
和 kwargs
是可选的。
ast.FunctionDef()
ast.FunctionDef(name, args, body, decorator_list, returns, type_comment)
一个函数定义。
name
是函数名称的原始字符串。args
是一个arguments
节点。body
是函数内部的节点列表。decorator_list
是要应用的装饰器列表,最外层的最先保存(即列表中的第一项将最后被应用)。returns
是返回标注。
type_comment
是带有以注释表示的类型标注的可选的字符串。
ast.ClassDef()
ast.ClassDef(name, bases, keywords, starargs, kwargs, body, decorator_list)
一个类定义。
name
为类名称的原始字符串。bases
为一个由显式指明的基类节点组成的列表。keywords
is a list ofkeyword
nodes, principally for ‘metaclass’. Other keywords will be passed to the metaclass, as per PEP-3115.starargs
和kwargs
各为一个单独的节点,与在函数调用中的一致。 starargs 将被展开加入到基类的列表中,而 kwargs 将被传给元类。body
是一个由代表类定义内部代码的节点组成的列表。decorator_list
是一个节点的列表,与FunctionDef
中的一致。
关键函数
ast.parse()
ast.parse(source, filename='<unknown>’, mode=’exec’, *, type_comments=False, feature_version=None)
把源码解析成ast节点
ast.unparse()
反向解析一个 ast.AST
对象并生成一个包含当再次使用 ast.parse()
解析时将产生同样的 ast.AST
对象的代码的字符串。
ast.dump()
ast.dump(node, annotate_fields=True, include_attributes=False, *, indent=None)
返回 node 中树结构的格式化转储。 这主要适用于调试目的。此处使用astunparse.dump()
更加方便于调试,因为该库中的dump
函数做了格式缩进和对齐
如何遍历ast节点
对于如何遍历ast节点,模块中给出了两个遍历类以及类中的遍历方法,可以通过对这两个类进行继承来自定义遍历操作
ast.NodeVisitor
一个遍历抽象语法树并针对所找到的每个节点调用访问器函数的节点访问器基类。 该函数可能会返回一个由 visit()
方法所提供的值。
这个类应当被子类化,并由子类来添加访问器方法。
访问一个节点。 默认实现会调用名为 self.visit_classname
的方法其中 classname 为节点类的名称,或者如果该方法不存在则为 generic_visit()
。
该访问器会在节点的所有子节点上调用 visit()
。
请注意所有包含自定义访问器方法的节点的子节点将不会被访问除非访问器调用了 generic_visit()
或是自行访问它们。
ast.NodeTransformer
子类 NodeVisitor
用于遍历抽象语法树,并允许修改节点。
NodeTransformer
将遍历抽象语法树并使用visitor方法的返回值去替换或移除旧节点。如果visitor方法的返回值为 None
, 则该节点将从其位置移除,否则将替换为返回值。当返回值是原始节点时,无需替换。
举例
现在我想遍历整个ast树,输出所有的函数定义名称,应该如何操作?
import ast
code="""
class a:
def __init__(self) -> None:
self.abc=123
def a(self):
self.abc=self.abc*2
def b(self):
self.abc=self.abc/2
"""
class Visitor(ast.NodeVisitor):
def __init__(self) -> None:
super().__init__()
def visit_FunctionDef(self,node):
print(node.name)
parsed=ast.parse(code)
test=Visitor()
test.visit(parsed)
自定义一个类,继承自NodeVisitor
类并自定义其visit函数,使其访问每个函数定义节点,输出节点名称,对源码进行解析后使用visit()
函数即可进行遍历。如果要修改节点的话就使用NodeTransformer
类,方法相同
参考文献
- https://docs.python.org/zh-cn/3/library/ast.html
- https://pycoders-weekly-chinese.readthedocs.io/en/latest/issue3/static-modification-of-python-with-python-the-ast-module.html
- https://peps.python.org/pep-0339/