您好,欢迎访问一九零五行业门户网

Python Ast抽象语法树应该如何使用?

引言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又有其特殊的方便性。
下面是一个抽象语法的简单实例。
module(body=[ print( dest=none, values=[binop( left=num(n=1),op=add(),right=num(n=2))], nl=true, )])
2. 创建ast2.1 compile函数先简单了解一下compile函数。
compile(source, filename, mode[, flags[, dont_inherit]])
source -- 字符串或者ast(abstract syntax trees)对象。一般可将整个py文件内容file.read()传入。
filename -- 代码文件名称,如果不是从文件读取代码则传递一些可辨认的值。
mode -- 指定编译代码的种类。可以指定为 exec, eval, single。
flags -- 变量作用域,局部命名空间,如果被提供,可以是任何映射对象。
flags和dont_inherit是用来控制编译源码时的标志。
func_def = \"""def add(x, y): return x + yprint add(3, 5)"""
使用compile编译并执行:
>>> cm = compile(func_def, '<string>', 'exec')>>> exec cm>>> 8
上面func_def经过compile编译得到字节码,cm即code对象,
true == isinstance(cm, types.codetype)。
compile(source, filename, mode, ast.pycf_only_ast) <==> ast.parse(source, filename='<unknown>', mode='exec')
2.2 生成ast使用上面的func_def生成ast.
r_node = ast.parse(func_def)print astunparse.dump(r_node) # print ast.dump(r_node)
下面是func_def对应的ast结构:
module(body=[ functiondef( name='add', args=arguments( args=[name(id='x',ctx=param()),name(id='y',ctx=param())], vararg=none, kwarg=none, defaults=[]), body=[return(value=binop( left=name(id='x',ctx=load()), op=add(), right=name(id='y',ctx=load())))], decorator_list=[]), print( dest=none, values=[call( func=name(id='add',ctx=load()), args=[num(n=3),num(n=5)], keywords=[], starargs=none, kwargs=none)], nl=true) ])
除了ast.dump,有很多dump ast的第三方库,如astunparse, codegen, unparse等。这些第三方库不仅能够以更好的方式展示出ast结构,还能够将ast反向导出python source代码。
module python version "$revision$"{ mod = module(stmt* body)| expression(expr body) stmt = functiondef(identifier name, arguments args, stmt* body, expr* decorator_list) | classdef(identifier name, expr* bases, stmt* body, expr* decorator_list) | return(expr? value) | print(expr? dest, expr* values, bool nl)| for(expr target, expr iter, stmt* body, stmt* orelse) expr = boolop(boolop op, expr* values) | binop(expr left, operator op, expr right)| lambda(arguments args, expr body)| dict(expr* keys, expr* values)| num(object n) -- a number as a pyobject. | str(string s) -- need to specify raw, unicode, etc?| name(identifier id, expr_context ctx) | list(expr* elts, expr_context ctx) -- col_offset is the byte offset in the utf8 string the parser uses attributes (int lineno, int col_offset) expr_context = load | store | del | augload | augstore | param boolop = and | or operator = add | sub | mult | div | mod | pow | lshift | rshift | bitor | bitxor | bitand | floordiv arguments = (expr* args, identifier? vararg, identifier? kwarg, expr* defaults)}
上面是部分摘自官网的 abstract grammar,实际遍历ast node过程中根据node的类型访问其属性。
3. 遍历astpython提供了两种方式来遍历整个抽象语法树。
3.1 ast.nodetransfer将func_def中的add函数中的加法运算改为减法,同时为函数实现添加调用日志。
class codevisitor(ast.nodevisitor): def visit_binop(self, node): 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) func_log_stmt = ast.print( dest = none, values = [ast.str(s = 'calling func: %s' % node.name, lineno = 0, col_offset = 0)], nl = true, lineno = 0, col_offset = 0, ) node.body.insert(0, func_log_stmt) r_node = ast.parse(func_def) visitor = codevisitor() visitor.visit(r_node) # print astunparse.dump(r_node) print astunparse.unparse(r_node) exec compile(r_node, '<string>', 'exec')
运行结果:
function name:adddef add(x, y): print 'calling func: add' return (x - y)print add(3, 5)calling func: add-2
3.2 ast.nodetransformer使用nodevisitor主要是通过修改语法树上节点的方式改变ast结构,nodetransformer主要是替换ast中的节点。
既然func_def中定义的add已经被改成一个减函数了,那么我们就彻底一点,把函数名和参数以及被调用的函数都在ast中改掉,并且将添加的函数调用log写的更加复杂一些,争取改的面目全非:-)
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) if node.name == 'add': node.name = 'sub' args_num = len(node.args.args) args = tuple([arg.id for arg in node.args.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)) 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 r_node = ast.parse(func_def) transformer = codetransformer() r_node = transformer.visit(r_node) # print astunparse.dump(r_node) source = astunparse.unparse(r_node) print source # exec compile(r_node, '<string>', 'exec') # 新加入的node func_log_stmt 缺少lineno和col_offset属性 exec compile(source, '<string>', 'exec') exec compile(ast.parse(source), '<string>', 'exec')
结果:
def sub(a, b): print 'calling func: sub', 'args:', a, b return (a - b)print sub(3, 5)calling func: sub args: 3 5-2calling func: sub args: 3 5-2
代码中能够清楚的看到两者的区别。这里不再赘述。
4.ast应用ast模块实际编程中很少用到,但是作为一种源代码辅助检查手段是非常有意义的;语法检查,调试错误,特殊字段检测等。
上面通过为函数添加调用日志的信息是一种调试python源代码的一种方式,不过实际中我们是通过parse整个python文件的方式遍历修改源码。
4.1 汉字检测下面是中日韩字符的unicode编码范围
cjk unified ideographs
range: 4e00&mdash; 9fff
number of characters: 20992
languages: chinese, japanese, korean, vietnamese
使用 unicode 范围 \u4e00 - \u9fff 来判别汉字,注意这个范围并不包含中文字符(e.g. u';' == u'\uff1b') .
下面是一个判断字符串中是否包含中文字符的一个类cncheckhelper:
class cncheckhelper(object): # 待检测文本可能的编码方式列表 valid_encoding = ('utf-8', 'gbk') def _get_unicode_imp(self, value, idx = 0): if idx < len(self.valid_encoding): try: return value.decode(self.valid_encoding[idx]) except: return self._get_unicode_imp(value, idx + 1) def _get_unicode(self, from_str): if isinstance(from_str, unicode): return none return self._get_unicode_imp(from_str) def is_any_chinese(self, check_str, is_strict = true): unicode_str = self._get_unicode(check_str) if unicode_str: c_func = any if is_strict else all return c_func(u'\u4e00' <= char <= u'\u9fff' for char in unicode_str) return false
接口is_any_chinese有两种判断模式,严格检测只要包含中文字符串就可以检查出,非严格必须全部包含中文。
下面我们利用ast来遍历源文件的抽象语法树,并检测其中字符串是否包含中文字符。
class codecheck(ast.nodevisitor): def __init__(self): self.cn_checker = cncheckhelper() def visit_str(self, node): self.generic_visit(node) # if node.s and any(u'\u4e00' <= char <= u'\u9fff' for char in node.s.decode('utf-8')): if self.cn_checker.is_any_chinese(node.s, true): print 'line no: %d, column offset: %d, cn_str: %s' % (node.lineno, node.col_offset, node.s) project_dir = './your_project/script' for root, dirs, files in os.walk(project_dir): print root, dirs, files py_files = filter(lambda file: file.endswith('.py'), files) checker = codecheck() for file in py_files: file_path = os.path.join(root, file) print 'checking: %s' % file_path with open(file_path, 'r') as f: root_node = ast.parse(f.read()) checker.visit(root_node)
上面这个例子比较的简单,但大概就是这个意思。
关于cpython解释器执行源码的过程可以参考官网描述:pep 339
4.2 closure 检查一个函数中定义的函数或者lambda中引用了父函数中的local variable,并且当做返回值返回。特定场景下闭包是非常有用的,但是也很容易被误用。
关于python闭包的概念可以参考我的另一篇文章:理解python闭包概念
这里简单介绍一下如何借助ast来检测lambda中闭包的引用。代码如下:
class lambdacheck(ast.nodevisitor): def __init__(self): self.illegal_args_list = [] self._cur_file = none self._cur_lambda_args = [] def set_cur_file(self, cur_file): assert os.path.isfile(cur_file), cur_file self._cur_file = os.path.realpath(cur_file) def visit_lambda(self, node): """ lambda 闭包检查原则: 只需检测lambda expr body中args是否引用了lambda args list之外的参数 """ self._cur_lambda_args =[a.id for a in node.args.args] print astunparse.unparse(node) # print astunparse.dump(node) self.get_lambda_body_args(node.body) self.generic_visit(node) def record_args(self, name_node): if isinstance(name_node, ast.name) and name_node.id not in self._cur_lambda_args: self.illegal_args_list.append((self._cur_file, 'line no:%s' % name_node.lineno, 'var:%s' % name_node.id)) def _is_args(self, node): if isinstance(node, ast.name): self.record_args(node) return true if isinstance(node, ast.call): map(self.record_args, node.args) return true return false def get_lambda_body_args(self, node): if self._is_args(node): return # for cnode in ast.walk(node): for cnode in ast.iter_child_nodes(node): if not self._is_args(cnode): self.get_lambda_body_args(cnode)
遍历工程文件:
project_dir = './your project/script' for root, dirs, files in os.walk(project_dir): py_files = filter(lambda file: file.endswith('.py'), files) checker = lambdacheck() for file in py_files: file_path = os.path.join(root, file) checker.set_cur_file(file_path) with open(file_path, 'r') as f: root_node = ast.parse(f.read()) checker.visit(root_node) res = '\n'.join([' ## '.join(info) for info in checker.illegal_args_list]) print res
由于lambda(arguments args, expr body)中的body expression可能非常复杂,上面的例子中仅仅处理了比较简单的body expr。可根据自己工程特点修改和扩展检查规则。为了更加一般化可以单独写一个visitor类来遍历lambda节点。
以上就是python ast抽象语法树应该如何使用?的详细内容。
其它类似信息

推荐信息