Python ast module

由于一个工作需要把大量的vbscript脚本转换成python脚本, 需要学习一下python的ast模块, 本文是一些干货的笔记. 由于当前最新的python 3.8的官方文档过于简略, 主要参考和翻译绿树蛇的文档和python3.10pre的文档.

AST是Abstract Syntax Trees的缩写, 中文是抽象语法树.

AST和code的转换

python代码有三个分身,

  1. 以unicode字符串存在的源代码. 人类可读写, python解释器不可读.
  2. 以AST对象存在的抽象语法树. 人类可读写, 但很难读懂原来的语义, 用来显示python语义元素之间的关系. 是从源代码翻译成目标代码的中间产品.
  3. 以code object存在的可执行的目标代码. python解释器可读, 人类不可读.

source => AST: 使用ast.parse(). AST => object: 使用compile().

1
2
3
4
5
6
>>> import ast
>>> tree = ast.parse("print('hello world')")
>>> tree
<_ast.Module object at 0x00000000029D0820>
>>> exec(compile(tree, filename="<ast>", mode="exec"))
hello world

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.
  • Meta also tries to decompile Python bytecode to an AST, but it appears to be unmaintained.
  • uncompyle6 is 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
2
3
4
5
6
7
8
9
10
11
12
13
14
>>> astpp.parseprint('f"sin({a}) is {sin(a):.3}."')
Module(body=[
Expr(value=JoinedStr(values=[
Constant(value='sin(', kind=None),
FormattedValue(value=Name(id='a', ctx=Load()), conversion=-1, format_spec=None),
Constant(value=') is ', kind=None),
FormattedValue(value=Call(func=Name(id='sin', ctx=Load()), args=[
Name(id='a', ctx=Load()),
], keywords=[]), conversion=-1, format_spec=JoinedStr(values=[
Constant(value='.3', kind=None),
])),
Constant(value='.', kind=None),
])),
], type_ignores=[])

注意上面使用的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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
>>> astpp.parseprint('a')
Module(body=[
Expr(value=Name(id='a', ctx=Load())),
], type_ignores=[])
>>> astpp.parseprint('a=1')
Module(body=[
Assign(targets=[
Name(id='a', ctx=Store()),
], value=Constant(value=1, kind=None), type_comment=None),
], type_ignores=[])
>>> astpp.parseprint('del a')
Module(body=[
Delete(targets=[
Name(id='a', ctx=Del()),
]),
], type_ignores=[])
>>> astpp.parseprint('a,*b=it')
Module(body=[
Assign(targets=[
Tuple(elts=[
Name(id='a', ctx=Store()),
Starred(value=Name(id='b', ctx=Store()), ctx=Store()),
], ctx=Store()),
], value=Name(id='it', ctx=Load()), type_comment=None),
], type_ignores=[])

Expressions 表达式

class ast.Expr(value)

如果一个表达式本身作为语句出现, 并且表达式的返回值没有被使用或者被存贮, 表达式将会被放到这个类的节点中.

value将为其他表达式节点, 常量节点, Name节点, Lambda节点, Yield节点, YieldFrom节点之一.

1
2
3
4
>>> astpp.parseprint('-a')
Module(body=[
Expr(value=UnaryOp(op=USub(), operand=Name(id='a', ctx=Load()))),
], type_ignores=[])

一元运算

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
>>> astpp.parseprint('a <= b < c')
Module(body=[
Expr(value=Compare(left=Name(id='a', ctx=Load()), ops=[
LtE(),
Lt(),
], comparators=[
Name(id='b', ctx=Load()),
Name(id='c', ctx=Load()),
])),
], type_ignores=[])
>>> astpp.parseprint('a == b ')
Module(body=[
Expr(value=Compare(left=Name(id='a', ctx=Load()), ops=[
Eq(),
], comparators=[
Name(id='b', ctx=Load()),
])),
], type_ignores=[])

函数调用

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
2
3
4
>>> astpp.parseprint('obj.attr')
Module(body=[
Expr(value=Attribute(value=Name(id='obj', ctx=Load()), attr='attr', ctx=Load())),
], type_ignores=[])

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
>>> astpp.parseprint("[ord(c) for line in file for c in line]", mode='eval') # Multiple comprehensions in one.
Expression(body=ListComp(elt=Call(func=Name(id='ord', ctx=Load()), args=[
Name(id='c', ctx=Load()),
], keywords=[]), generators=[
comprehension(target=Name(id='line', ctx=Store()), iter=Name(id='file', ctx=Load()), ifs=[], is_async=0),
comprehension(target=Name(id='c', ctx=Store()), iter=Name(id='line', ctx=Load()), ifs=[], is_async=0),
]))
>>> astpp.parseprint("(n**2 for n in it if n>5 if n<10)", mode='eval') # Multiple if clauses
Expression(body=GeneratorExp(elt=BinOp(left=Name(id='n', ctx=Load()), op=Pow(), right=Constant(value=2, kind=None)), generators=[
comprehension(target=Name(id='n', ctx=Store()), iter=Name(id='it', ctx=Load()), ifs=[
Compare(left=Name(id='n', ctx=Load()), ops=[
Gt(),
], comparators=[
Constant(value=5, kind=None),
]),
Compare(left=Name(id='n', ctx=Load()), ops=[
Lt(),
], comparators=[
Constant(value=10, kind=None),
]),
], is_async=0),
]))

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()), 会有异常抛出.


  1. 注意ast.Expressionast.Expr不同.↩︎