Python AST 抽象语法树

Abstract Sytax Tree
暂时用到的原因:在模型量化中,需要量化某些操作符带来的运算效果,比如 '+', '-','*', '/' 等等,这些就需要对源代码进行查询,因此就要需要将python解释器已经将源代码转化为运行的类后,再翻转回源代码
参考:
https://docs.python.org/3/library/ast.html#ast.NodeTransformer
https://www.cnblogs.com/yssjun/p/10069199.html
Abstract Syntax Trees即抽象语法树。Ast是python源码到字节码的一种中间产物,借助ast模块可以从语法树的角度分析源码结构。此外,我们不仅可以修改和执行语法树,还可以将Source生成的语法树unparse成python源码。因此ast给python源码检查、语法分析、修改代码以及代码调试等留下了足够的发挥空间。

1. AST简介

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) --> 控制流程图 --> 字节码

上述过程在python2.5之后被应用。python源码首先被解析成语法树,随后又转换成抽象语法树。在抽象语法树中我们可以看到源码文件中的python的语法结构。
大部分时间编程可能都不需要用到抽象语法树,但是在特定的条件和需求的情况下,AST又有其特殊的方便性。

下面是一个抽象语法的简单实例。

func_def = \
""" 
def add(x, y):
    return x+y
    
print(add(3,5))
"""
print(func_def)

其中 三引号可以根据书写的方式智能换行,输出如下:


def add(x, y):
    return x+y
    
print(add(3,5))

image.png

2. 创建AST

2.1 compile(source, filename, mode[, flags[, dont_inherit]])

这是python自带的函数

  • source -- 字符串或者AST(Abstract Syntax Trees)对象。一般可将整个py文件内容file.read()传入。
  • filename -- 代码文件名称,如果不是从文件读取代码则传递一些可辨认的值。
  • mode -- 指定编译代码的种类。可以指定为 exec, eval, single。
  • flags -- 变量作用域,局部命名空间,如果被提供,可以是任何映射对象。
  • flags和dont_inherit是用来控制编译源码时的标志。
>>> cm = compile(func_def, filename='<string>', mode='exec')
>>> exec(cm)
8
>>> type(cm)
code

上面func_def经过compile编译得到字节码,cm即code对象,True == isinstance(cm, types.CodeType)。

2.2 生成AST

>>> cm1 = ast.parse(func_def,filename='<unknown>', mode='exec')
>>> type(cm1)
_ast.Module
>>> ast.dump(cm1)
(
body=[
    FunctionDef(name='add', 
                args=arguments(
                                args=[arg(arg='x', annotation=None), arg(arg='y', annotation=None)], 
                                vararg=None, kwonlyargs=[], kw_defaults=[], kwarg=None, defaults=[]
                              ), 
                body=[Return(
                                value=BinOp(left=Name(id='x', ctx=Load()), op=Add(), right=Name(id='y', ctx=Load()))
                            )
                     ], 
                decorator_list=[], 
                returns=None), 
    Expr(value=Call(
                    func=Name(id='print', ctx=Load()), 
                    args=[Call(func=Name(id='add', ctx=Load()), args=[Num(n=3), Num(n=5)], keywords=[])], 
                    keywords=[])
                    )
    ]
)

可以看到,这里对源代码进行了解析

  • 首先是源代码字符串的主体 body,可以看到,一个是FunctionDef,也就是我们定义的add函数,另外一个是下面使用的print函数
  • 对于第一个主体 FunctionDef,可以看到里面的 name是 ‘add’,也就是函数的名字是 add, 再一个就是args,参数,可以看到一个是 'x',annotation=None,另外一个参数是 y, annntation=None; 然后里面又有一个body,里面可以看到是 return返回值,其中BinOp表示双目操作符,操作符的左值为x,操作符 opAdd(),也就是将我们源代码中的 +转换成了 Add()函数,最后就是右值 y
  • 最后就是 print函数,可以看到,valuesCall 调用了一个函数,其中 函数名func为 add,参数有两个,一个是3,一个是5

3. 遍历语法树

python提供了两种方式来遍历整个语法树

  • 节点的访问就只需要重写 visit_nodename函数,在里面定义参数即可
  • 这里节点的visit 会默认根据 ast中的 nodename 去访问 visit_nodename 函数,同时如果当前节点存- 在children,比如 FunctionDef 中存在 BinOp 节点,若想 visit BinOp这个节点,就需要在 FunctionDef中增加一句 self.generic_visit()来达到递归访问;如果不加,就只能访问当前节点

generic_visit(node)
This visitor calls visit() on all children of the node. Note that child nodes of nodes that have a custom visitor method won’t be visited unless the visitor calls generic_visit() or visits them itself.

3.1 ast.NodeVisitor

比如 我们将 func_def 的 add 函数中的加法运算改为减法

class CodeVisitor(ast.NodeVisitor):
    def visit_BinOp(self, node):# 这个函数的访问是由于 Visit_FunctionDef的先访问再generic_visit才访问的
        print('Bin')            # 如果Visit_FunctionDef中没有generic_visit的话,则这个函数是不会访问的
        if isinstance(node.op, ast.Add):
            node.op = ast.Sub()
            
        self.generic_visit(node)
    
    def visit_FunctionDef(self, node):
        print('Function Name: %s'% node.name)
        self.generic_visit(node) # FunctionDef中还包含有 BinOp,因此会进去visit BinOP
        
    def visit_Call(self, node):
        print("call") 
        self.generic_visit(node) # 因为AST的Call中还包含有一个Call,因此会重复再访问一次 
    
        
r_node = ast.parse(func_def)
visitor = CodeVisitor()
visitor.visit(r_node) # 这里的visit函数会根据 node 的语法树去遍历里面的函数,

输出:

Function Name: add
Bin
call
call

3.2 ast.NodeTransformer

A NodeVisitor subclass that walks the abstract syntax tree and allows modification of nodes

使用NodeVisitor主要是通过修改语法树上节点的方式改变AST结构,NodeTransformer主要是替换ast中的节点。

class CodeTransformer(ast.NodeTransformer):
    def visit_BinOp(self, node):
        if isinstance(node.op, ast.Add):
            node.op = ast.Sub()
        self.generic_visit(node)
        return node

    def visit_FunctionDef(self, node):
        self.generic_visit(node) # 这里表示先去访问里面的children node        
        if node.name == 'add':
            node.name = 'sub'
        args_num = len(node.args.args)
        
        args_num = len(node.args.args)
        args = tuple([arg.arg for arg in node.args.args])
        print(str(args))
        func_log_stmt = ''.join(["print('calling func: %s', " % node.name, "'args:'", ", %s" * args_num % args ,')'])
        node.body.insert(0, ast.parse(func_log_stmt))
        
        #func_log_stmt = ''.join(["print 'calling func: %s', " % node.name, "'args:'", ", %s" * args_num % args])
        #node.body.insert(0, ast.parse(func_log_stmt))

        return node

    def visit_Name(self, node):
        replace = {'add': 'sub', 'x': 'a', 'y': 'b'}
        re_id = replace.get(node.id, None)
        node.id = re_id or node.id
        self.generic_visit(node)
        return node
    
    def visit_arg(self, node):
        self.generic_visit(node)
        replace = {'x':'a', 'y':'b'}
        node.arg = replace[node.arg]
        return node
        
        
r_node = ast.parse(func_def)
transformer = CodeTransformer()
r_node = transformer.visit(r_node)
#print(astunparse.dump(r_node))
source = astunparse.unparse(r_node) # astunparse 一般python不自带,需要conda 或者 pip安装
print(source)

输出:

('a', 'b')


def sub(a, b):
    print('calling func: sub', 'args:', a, b)
    return (a - b)
print(sub(3, 5))

可以看加入了一个print语句,然后将变量名字由 x, y 改为了 a, b


Keep in mind that if the node you’re operating on has child nodes you must either transform the child nodes yourself or call the generic_visit() method for the node first.


Don’t use the NodeVisitor if you want to apply changes to nodes during traversal. For this a special visitor exists (NodeTransformer) that allows modifications.

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,881评论 4 368
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 68,052评论 1 301
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 110,598评论 0 250
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,407评论 0 217
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,823评论 3 294
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,872评论 1 224
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,037评论 2 317
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,778评论 0 204
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,505评论 1 247
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,745评论 2 253
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,233评论 1 264
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,568评论 3 260
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,231评论 3 241
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,141评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,939评论 0 201
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,954评论 2 283
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,784评论 2 275

推荐阅读更多精彩内容