Python ast module
由于一个工作需要把大量的vbscript脚本转换成python脚本, 需要学习一下python的ast模块, 本文是一些干货的笔记. 由于当前最新的python 3.8的官方文档过于简略, 主要参考和翻译绿树蛇的文档和python3.10pre的文档.
AST是Abstract Syntax Trees的缩写, 中文是抽象语法树.
AST和code的转换
python代码有三个分身,
- 以unicode字符串存在的源代码. 人类可读写, python解释器不可读.
- 以AST对象存在的抽象语法树. 人类可读写, 但很难读懂原来的语义, 用来显示python语义元素之间的关系. 是从源代码翻译成目标代码的中间产品.
- 以code object存在的可执行的目标代码. python解释器可读, 人类不可读.
source => AST: 使用ast.parse(). AST => object:
使用compile().
| 1 | import ast | 
Modes
源代码到AST的编译过程可以以三种不同模式进行.
AST的根节点依赖于传递给ast.parse()的mode参数,
并且在从AST到目标代码的编译过程中传递给compile()传递相同的mode参数.
- exec - 默认值是mode='exec'. AST的根节点是ast.Module, 根节点的属性body是一个子节点的list.
- eval -
单独的表达式可以被编译为mode='eval'的AST对象, AST的根是一个ast.Expression1, 他的属性body是一个单节点, 例如ast.Call或者ast.BinOp. 把这个AST对象传递给eval()将会返回表达式的值.
- single -
单独的语句或者表达式可以被编译为mode='single'的AST对象, AST的根是ast.Interactive, 它的属性body是一个子节点的list. 如果这是一个表达式,sys.displayhook()将会被调用并传入表达式的结果, 就如同python的交互式shell被调用了一样.
修正行列号
编译出的AST对象,
每一个节点都必须有行列号属性lineno并且col_offset.
从普通源代码编译而来的AST对象已经具有了行列号.
但是在程序中动态创建的节点没有行列好,
有几个helper functions可以做修正行列号.
- ast.fix_missing_locations()通过父节点的行列号递归的修正所有缺失行列号的节点.
- ast.copy_location()用于从另一个节点拷贝行列号. 特别是当做节点替换的时候有用.
- ast.increment_lineno()增加一个节点和其子节点的行号, 一般用于把代码块移动到其他位置.
反向转化
Python标准库不提供一种从目标码到AST, 或者从AST到源码转化的办法. 但一些第三方工具可以实现这些:
- astor可以把AST转化为可读的Python code.
- Metaalso tries to decompile Python bytecode to an AST, but it appears to be unmaintained.
- uncompyle6is an actively maintained Python decompiler at the time of writing. Its documented interface is a command line program producing Python source code.
AST节点
AST节点把代码里的每一个元素表达为一个对象. 这些对象都是不同的AST子类的实例. 后面将会对每一种node对象逐一描述.
Literals (字面量)
Before Python 3.8
在python 3.8以前的一部字面量的类, 在python3.8的时候已经不建议使用了, 下面列出这几个类
- class ast.Num(n), 用来表示整数, 浮点数, 复数
- class ast.Str(n), 用来表示字符串
- class ast.Bytes(s), 用来表示bytes类型
- class ast.Ellipsis, 用来表示内置常量...Ellipsis
- class ast.NameConstant(value), 用来表示内置常量True,False, 或者None.
上面这几个在Python3.8的时候被ast.Constant代替,
而且在未来的python版本中有可能被删除.
class ast.Constant(value, kind)
- New in version 3.6.
- Changed in version 3.8: The kind field was added.
常量. value可以是上面提到几种常量,
也可以是immutable容器tuples和frozensets, 但要求容器中的元素都是常量.
f-string
Python3.6引入跟f-string对应的AST节点由下面两个组成
- class ast.FormattedValue(value, conversion, format_spec)
- class ast.JoinedStr(values)
看下面的例子可知, JoinedStr可以被看成一个list, 元素是字符串和FormattedValue组合而成. FormattedValue的format_spec的值是一个JoinedStr.
| 1 | astpp.parseprint('f"sin({a}) is {sin(a):.3}."') | 
注意上面使用的astpp类可以在绿树蛇的git repo里找到, 将来在python3.9以后可以用ast.dump直接显示类似的结果.
容器类
- class ast.List(elts, ctx)
- class ast.Tuple(elts, ctx)
- class ast.Set(elts)
这里elts代表每个元素节点的一个list, ctx可以取两个值ast.Load和ast.Store之一. 当容器为赋值语句的目标的时候是ast.Store, 其他的时候就是ast.Load.
- class ast.Dict(keys, values)
这里keys和values, 分别是字典的键值的元素AST节点的list, 要按照顺序对应.
变量
- class ast.Name(id, ctx): 变量名, id是一个字符串代表变量名 
- class ast.Starred(value, ctx): 代表*var, value是var的AST节点 - ctx 可以是下面三个值之一 
- class ast.Load 
- class ast.Store 
- class ast.Del 
| 1 | astpp.parseprint('a') | 
Expressions 表达式
class ast.Expr(value)
如果一个表达式本身作为语句出现, 并且表达式的返回值没有被使用或者被存贮, 表达式将会被放到这个类的节点中.
value将为其他表达式节点, 常量节点, Name节点, Lambda节点, Yield节点, YieldFrom节点之一.
| 1 | astpp.parseprint('-a') | 
一元运算
class ast.UnaryOp(op, operand)
运算符op是如下几个类节点之一, operand是任何表达式
- class ast.UAdd
- class ast.USub
- class ast.Not
- class ast.Invert
二元运算
class ast.BinOp(left, op, right)
运算符op是如下几个类节点之一, left和right是任何表达式
- class ast.Add
- class ast.Sub
- class ast.Mult
- class ast.Div
- class ast.FloorDiv
- class ast.Mod
- class ast.Pow
- class ast.LShift¶
- class ast.RShift
- class ast.BitOr
- class ast.BitXor
- class ast.BitAnd
- class ast.MatMult
布尔运算
class ast.BoolOp(op, values)
op是下面的Or或者And. values是参与运算的节点的list, 注意这个list里可以多余两个元素, 表示连续的and或or.
- class ast.And
- class ast.Or
比较
class ast.Compare(left, ops, comparators)
可以用来表达两个或者多个值的比较. left是第一个比较的元素, ops是一个比较运算符的list, comparators是出了第一个元素以外的元素的列表.
可用的比较操作符有:
- class ast.Eq
- class ast.NotEq
- class ast.Lt
- class ast.LtE
- class ast.Gt
- class ast.GtE
- class ast.Is
- class ast.IsNot
- class ast.In
- class ast.NotIn
例子:
| 1 | astpp.parseprint('a <= b < c') | 
函数调用
class ast.Call(func, args, keywords, starargs, kwargs)
func代表函数, 通常是一个Name或者Attribute对象. args是pass by postion的参数的列表, keywords是一个keyword对象的列表表示pass by keyword的参数.
class ast.keyword(arg, value)
arg表示形参的名字, value是传入的实参.
class ast.IfExp(test, body, orelse)
if表达式是如同a if b else c的表达式, test, body,
orelse都是一个单独的node.
class ast.Attribute(value, attr, ctx)
d.keys形式的表达式. value是一个节点, 典型的是一个Name.
attr是一个字符串表示给定attribute的名字, ctx是Load,
Store或者Del之一.
| 1 | astpp.parseprint('obj.attr') | 
class ast.NamedExpr(target, value)
python 3.8引入的赋值表达式:=
Subscripting (下标访问)
class ast.Subscript(value, slice, ctx)
形如a[1], a['x']的运算.
value是被下标访问的对象, 通常是一个sequence或者mapping. slice是Index,
Slice或者ExtSlice之一.
在未来的python3.9之后Index和ExtSlice都被移除了, 将合并到Slice里.
- class ast.Index(value) : 单值索引
- class ast.Slice(lower, upper, step) : 切片
- class ast.ExtSlice(dims) : 切片的扩展, dims是一个Slice和Index的list.
切片的扩展, dims是一个Slice和Index的list.
Comprehensions(不知道标准翻译是啥, 推导式?)
- class ast.ListComp(elt, generators)
- class ast.SetComp(elt, generators)
- class ast.GeneratorExp(elt, generators)
- class ast.DictComp(key, value, generators)
分别代表list comprehensions, set comprehensions, generator expressions, 和dictionary comprehensions.
elt(或key-value)是单节点, 表示将被推导的变量. generators是一个comprehension节点的list.
class ast.comprehension(target, iter, ifs, is_async)
一个用于推导的for语句. target用于表示每个元素, 通常是Name或者Tuple节点, iter是可迭代的对象. ifs是一个test expressions的列表, 每个for语句可以能有多个ifs.
New in version 3.6: is_async indicates a comprehension is asynchronous (using an async for instead of for). The value is an integer (0 or 1).
| 1 | astpp.parseprint("[ord(c) for line in file for c in line]", mode='eval') # Multiple comprehensions in one. | 
Statements 语句
下面罗列一下所有的语句节点的类, 具体内容以后用到了再填写.
Assgin 赋值语句
- class ast.Assign(targets, value, type_comment)
- class ast.AnnAssign(target, annotation, value, simple)
- class ast.AugAssign(target, op, value)
Other
- class ast.Raise(exc, cause)
- class ast.Assert(test, msg)
- class ast.Delete(targets)
- class ast.Pass
Imports
- class ast.Import(names)
- class ast.ImportFrom(module, names, level)
- class ast.alias(name, asname)
Control flow
- class ast.If(test, body, orelse)
- class ast.For(target, iter, body, orelse, type_comment)
- class ast.While(test, body, orelse)
- class ast.Break
- class ast.Continue
- class ast.Try(body, handlers, orelse, finalbody)
- class ast.ExceptHandler(type, name, body)
- class ast.With(items, body, type_comment)
- class ast.withitem(context_expr, optional_vars)
Function and class definitions
- class ast.FunctionDef(name, args, body, decorator_list, returns, type_comment)
- class ast.Lambda(args, body)
- class ast.arguments(posonlyargs, args, vararg, kwonlyargs, kw_defaults, kwarg, defaults)
- class ast.arg(arg, annotation, type_comment)
- class ast.Return(value)
- class ast.Yield(value)
- class ast.YieldFrom(value)
- class ast.Global(names)
- class ast.Nonlocal(names)
- class ast.ClassDef(name, bases, keywords, starargs, kwargs, body, decorator_list)
Async and await
- class ast.AsyncFunctionDef(name, args, body, decorator_list, returns, type_comment)
- class ast.Await(value)
- class ast.AsyncFor(target, iter, body, orelse, type_comment)
- class ast.AsyncWith(items, body, type_comment)
Top level nodes
- class Module(stmt* body, type_ignore *type_ignores)
- class Interactive(stmt* body)
- class Expression(expr body)
操作AST
对于已存在的AST, ast模块为我们提供了多种helper函数帮助我们在一个庞大的树中找到我们想要找到的枝叶.
class ast.NodeVisitor
这是一个节点访问的基类, 可以帮助我们遍历树的节点,
并且为每一个不同种类的节点调用不同的处理函数.
使用方法是首先以此类为基类创建一个访客子类, 为访客子类编写访客函数.
访客函数就是是override基类的一些方法, 形如visit_classname,
这里classname就是前一章里提到的所有节点类.
这个类有两个不需要被override的方法
- visit(node)
- generic_visit(node)
 visit(node)是访问某个节点的主函数, 其默认执行逻辑逻辑如下:
{% mermaid graph TB %} id0((start)) -->
id1["visit(node)方法检查输入的节点类"] id1 -->
id2{"访客类是否实现了该节点
的visit_classname()方法?"} id2 --Y-->
id3["调用visit_classname(node)方法"] id2 --N-->
id4["调用generic_visit(node)方法"] id4 --> id5["遍历每一个子节点"]
id3 --> id6{"定制化visit方法是否
调用generic_visit(node)方法?"}
id6 --Y--> id4 id6 --N--> id7((stop)) id5 -->
id8{"是否还有没访问的子节点"} id8 --Y-->
id9["选择下一个子节点,
以其为参数调用visit(node)"] id9 --> id1
id8 --N--> id7 {% endmermaid %}
注意, 1. 如果想要修改某些节点, 请不要用NodeVisitor这个类, 而要用NodeTransformer这个类. 2. Deprecated since version 3.8: Methods visit_Num(), visit_Str(), visit_Bytes(), visit_NameConstant() and visit_Ellipsis() are deprecated now and will not be called in future Python versions. Add the visit_Constant() method to handle all constant nodes.
其他遍历方法
一些其他的helper函数可以帮我们访问到我们想要的节点.
- ast.walk(node): 递归的产生一个列表, 包括以node为根节点整棵树的所有节点, 但顺序无法保证. 这个一般用于想要访问或者修改某些节点, 但不关心这些节点的上下文.
- ast.iter_child_nodes(node): 产生一个节点的所有子节点的列表, 这里包括该节点类的单节点属性和节点列表属性的所有元素.
- 当然你永远都可以直接通过一个节点的属性来访问其他节点. 这时, 所有的python关于迭代和索引的工具都是有效的. 特别是, isinstance()对于检查到底是什么节点非常有用.
监测节点
ast module提供几个helper函数帮忙检测节点.
- ast.iter_fields(node): 为节点的node._fields里的每个属性,产生一个一个tuple, (fieldname, value).
- ast.get_docstring(node, clean=True): 返回一个节点(类型必须为FunctionDef, AsyncFunctionDef, ClassDef, 或者Module)的docstring. 如果没有docstring, 则返回None.
- ast.dump(node, annotate_fields=True, include_attributes=False, *, indent=None): Changed in version 3.9: Added the indent option.
修改AST
修改AST的关键工具是基类class ast.NodeTransformer. 这个类的工作方式跟NodeVisitor非常像. 唯一的不同是visit_classname()方法的返回值如果是一个节点对象, 原节点将被返回的节点替代. 如果返回值是None, 原节点将被删除. 如果返回原节点, 那么原节点的替代将不会发生.
如果想要操作输入节点的子节点, 你必须或者手动修改子节点, 或者在返回前先调用generic_visit()方法.
需要小心的是, 如果你的修改和删除产生了无效的AST, python不会报错. 只有当你尝试编译AST成目标代码时(调用compile()), 会有异常抛出.
- 注意 - ast.Expression跟- ast.Expr不同.↩︎