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. 创建AST
2.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 + y
print 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. 遍历AST
python提供了两种方式来遍历整个抽象语法树。
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:add
def 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
-2
calling func: sub args: 3 5
-2
代码中能够清楚的看到两者的区别。这里不再赘述。
4.AST应用
AST模块实际编程中很少用到,但是作为一种源代码辅助检查手段是非常有意义的;语法检查,调试错误,特殊字段检测等。
上面通过为函数添加调用日志的信息是一种调试python源代码的一种方式,不过实际中我们是通过parse整个python文件的方式遍历修改源码。
4.1 汉字检测
下面是中日韩字符的unicode编码范围
CJK Unified Ideographs
- Range: 4E00— 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节点。
Ast的应用不仅限于上面的例子,限于篇幅,先介绍到这里。期待ast能帮助你解决一些比较棘手的问题。
Python Ast介绍及应用的更多相关文章
- 从零开始学Python第0周:Python基本介绍(部分内容来源于网络)
Python入门介绍 一,Python的基本介绍 (1)概要 Python是一种解释型,面向对象,动态数据类型的高级程序设计语言.常被广泛用于处理系统管理任务和web编程.现如今Python已经成为了 ...
- 【转】Python Twisted介绍
Python Twisted介绍 作者:Jessica McKellar 原文链接 Twisted是用Python实现的基于事件驱动的网络引擎框架.Twisted诞生于2000年初,在当时的网络游戏开 ...
- 【美妙的Python之中的一个】Python简单介绍及环境搭建
美妙的Python之Python简单介绍及安装 简而言之: Python 是能你无限惊喜的语言,与众不同. 1.Python: ...
- python模块介绍- multi-mechanize 性能测试工具
python模块介绍- multi-mechanize 性能测试工具 2013-09-13 磁针石 #承接软件自动化实施与培训等gtalk:ouyangchongwu#gmail.comqq 3739 ...
- python模块介绍- xlwt 创建xls文件(excel)
python模块介绍- xlwt 创建xls文件(excel) 2013-06-24磁针石 #承接软件自动化实施与培训等gtalk:ouyangchongwu#gmail.comqq 37391319 ...
- python模块介绍- binascii 二进制和ASCII转换
python模块介绍-binascii二进制和ASCII转换 目录 项目简介 简介: Uu编码 Binhex编码 Base64编码 QP码 CRC校验和 二进制转换 其他实例 项目简介 Python中 ...
- python模块介绍- HTMLParser 简单的HTML和XHTML解析器
python模块介绍- HTMLParser 简单的HTML和XHTML解析器 2013-09-11 磁针石 #承接软件自动化实施与培训等gtalk:ouyangchongwu#gmail.comqq ...
- 利用Python进行数据分析——重要的Python库介绍
利用Python进行数据分析--重要的Python库介绍 一.NumPy 用于数组执行元素级计算及直接对数组执行数学运算 线性代数运算.傅里叶运算.随机数的生成 用于C/C++等代码的集成 二.pan ...
- Python lambda介绍
转自:http://www.cnblogs.com/evening/archive/2010/03/29/2423554.html Python lambda 介绍 在学习python的过程中,l ...
随机推荐
- 华盛顿邮报:FBI 屡次夸大了“手机加密威胁”的数字
<华盛顿邮报>周二报道称,美国联邦调查局(FBI)严重夸大了由加密手机所造成的问题.以去年为例,该机构调查人员声称被大约 7800 部涉嫌犯罪活动的加密设备挡在了门外,而准确的数字应该在 ...
- redis与CPU、内存
任何一个后端应用,包括代码都要考虑对于CPU和内存的影响.redis本质上类似于nodejs,单进程.单线程,事件驱动,但不同的是redis是CPU密集型的.这里列出了redis与内存CPU的相关考虑 ...
- 【STM32H7教程】第5章 STM32H7下载和调试方法(MDK5)
完整教程下载地址:http://forum.armfly.com/forum.php?mod=viewthread&tid=86980 第5章 STM32H7下载和调试方法(MDK5) 本 ...
- ASP.NET Core2.2+Quartz.Net 实现web定时任务
作为一枚后端程序狗,项目实践常遇到定时任务的工作,最容易想到的的思路就是利用Windows计划任务/wndows service程序/Crontab程序等主机方法在主机上部署定时任务程序/脚本. 但是 ...
- Netty基础系列(2) --彻底理解阻塞非阻塞与同步异步的区别
引言 在进行I/O学习的时候,阻塞和非阻塞,同步和异步这几个概念常常被提及,但是很多人对这几个概念一直很模糊.要想学好Netty,这几个概念必须要掌握清楚. 同步和异步 同步与异步的区别在于,异步基于 ...
- 带你找到五一最省的旅游路线【dijkstra算法推导详解】
前言 五一快到了,小张准备去旅游了! 查了查到各地的机票 因为今年被扣工资扣得很惨,小张手头不是很宽裕,必须精打细算.他想弄清去各个城市的最低开销. [嗯,不用考虑回来的开销.小张准备找警察叔叔说自己 ...
- C#读写Excel的几种方法
1 使用Office自带的库 前提是本机须安装office才能运行,且不同的office版本之间可能会有兼容问题,从Nuget下载 Microsoft.Office.Interop.Excel 读写代 ...
- 【Android】用Cubism 2制作自己的Live2D——官方App样例源码学习(1)!
前言- 上几篇文章,我们一个一个的研究了Cubism官方提供的Android使用Live2D的简单例子,但是依旧和大家平时见到的还是有很大差距的.在研究了代码差不多一周以后,我决定还是用文字的形式记录 ...
- 基于后端云的Android注册登录开发
APP开发离不开注册登录功能,但是注册登录功能开发需要后台数据库的支持,对于一些初学者或者对后台数据 不熟悉的同学来说可能会有些困难.本文介绍一下后端云: 1. Bmob是国内起步较早的云后端服务平台 ...
- Soot生成代码控制流图
Soot可以对代码进行分析,提供了多种字节码分析和变换功能,通过它可以进行过程内和过程间的分析优化,以及程序流图的生成,还能通过图形化的方式输出. http://www.brics.dk/SootGu ...