Python?Ast抽象語(yǔ)法樹(shù)的介紹及應(yīng)用詳解
引言
Abstract Syntax Trees即抽象語(yǔ)法樹(shù)。Ast是python源碼到字節(jié)碼的一種中間產(chǎn)物,借助ast模塊可以從語(yǔ)法樹(shù)的角度分析源碼結(jié)構(gòu)。
此外,我們不僅可以修改和執(zhí)行語(yǔ)法樹(shù),還可以將Source生成的語(yǔ)法樹(shù)unparse成python源碼。因此ast給python源碼檢查、語(yǔ)法分析、修改代碼以及代碼調(diào)試等留下了足夠的發(fā)揮空間。
1. AST簡(jiǎn)介
Python官方提供的CPython解釋器對(duì)python源碼的處理過(guò)程如下:
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)
即實(shí)際python代碼的處理過(guò)程如下:
源代碼解析 --> 語(yǔ)法樹(shù) --> 抽象語(yǔ)法樹(shù)(AST) --> 控制流程圖 --> 字節(jié)碼
上述過(guò)程在python2.5之后被應(yīng)用。python源碼首先被解析成語(yǔ)法樹(shù),隨后又轉(zhuǎn)換成抽象語(yǔ)法樹(shù)。在抽象語(yǔ)法樹(shù)中我們可以看到源碼文件中的python的語(yǔ)法結(jié)構(gòu)。
大部分時(shí)間編程可能都不需要用到抽象語(yǔ)法樹(shù),但是在特定的條件和需求的情況下,AST又有其特殊的方便性。
下面是一個(gè)抽象語(yǔ)法的簡(jiǎn)單實(shí)例。
Module(body=[ Print( dest=None, values=[BinOp( left=Num(n=1),op=Add(),right=Num(n=2))], nl=True, )])
2. 創(chuàng)建AST
2.1 Compile函數(shù)
先簡(jiǎn)單了解一下compile函數(shù)。
compile(source, filename, mode[, flags[, dont_inherit]])
- source -- 字符串或者AST(Abstract Syntax Trees)對(duì)象。一般可將整個(gè)py文件內(nèi)容file.read()傳入。
- filename -- 代碼文件名稱(chēng),如果不是從文件讀取代碼則傳遞一些可辨認(rèn)的值。
- mode -- 指定編譯代碼的種類(lèi)??梢灾付?exec, eval, single。
- flags -- 變量作用域,局部命名空間,如果被提供,可以是任何映射對(duì)象。
- flags和dont_inherit是用來(lái)控制編譯源碼時(shí)的標(biāo)志。
func_def = \ """ def add(x, y): return x + y print add(3, 5) """
使用Compile編譯并執(zhí)行:
>>> cm = compile(func_def, '<string>', 'exec') >>> exec cm >>> 8
上面func_def經(jīng)過(guò)compile編譯得到字節(jié)碼,cm即code對(duì)象,
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對(duì)應(yīng)的ast結(jié)構(gòu):
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的第三方庫(kù),如astunparse, codegen, unparse等。這些第三方庫(kù)不僅能夠以更好的方式展示出ast結(jié)構(gòu),還能夠?qū)st反向?qū)С鰌ython 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) }
上面是部分摘自官網(wǎng)的 Abstract Grammar,實(shí)際遍歷ast Node過(guò)程中根據(jù)Node的類(lèi)型訪問(wèn)其屬性。
3. 遍歷AST
python提供了兩種方式來(lái)遍歷整個(gè)抽象語(yǔ)法樹(shù)。
3.1 ast.NodeTransfer
將func_def中的add函數(shù)中的加法運(yùn)算改為減法,同時(shí)為函數(shù)實(shí)現(xiàn)添加調(diào)用日志。
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')
運(yùn)行結(jié)果:
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主要是通過(guò)修改語(yǔ)法樹(shù)上節(jié)點(diǎn)的方式改變AST結(jié)構(gòu),NodeTransformer主要是替換ast中的節(jié)點(diǎn)。
既然func_def中定義的add已經(jīng)被改成一個(gè)減函數(shù)了,那么我們就徹底一點(diǎn),把函數(shù)名和參數(shù)以及被調(diào)用的函數(shù)都在ast中改掉,并且將添加的函數(shù)調(diào)用log寫(xiě)的更加復(fù)雜一些,爭(zhēng)取改的面目全非:-)
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')
結(jié)果:
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
代碼中能夠清楚的看到兩者的區(qū)別。這里不再贅述。
4.AST應(yīng)用
AST模塊實(shí)際編程中很少用到,但是作為一種源代碼輔助檢查手段是非常有意義的;語(yǔ)法檢查,調(diào)試錯(cuò)誤,特殊字段檢測(cè)等。
上面通過(guò)為函數(shù)添加調(diào)用日志的信息是一種調(diào)試python源代碼的一種方式,不過(guò)實(shí)際中我們是通過(guò)parse整個(gè)python文件的方式遍歷修改源碼。
4.1 漢字檢測(cè)
下面是中日韓字符的unicode編碼范圍
CJK Unified Ideographs
Range: 4E00— 9FFF
Number of characters: 20992
Languages: chinese, japanese, korean, vietnamese
使用 unicode 范圍 \u4e00 - \u9fff 來(lái)判別漢字,注意這個(gè)范圍并不包含中文字符(e.g. u';' == u'\uff1b') .
下面是一個(gè)判斷字符串中是否包含中文字符的一個(gè)類(lèi)CNCheckHelper:
class CNCheckHelper(object): # 待檢測(cè)文本可能的編碼方式列表 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有兩種判斷模式,嚴(yán)格檢測(cè)只要包含中文字符串就可以檢查出,非嚴(yán)格必須全部包含中文。
下面我們利用ast來(lái)遍歷源文件的抽象語(yǔ)法樹(shù),并檢測(cè)其中字符串是否包含中文字符。
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)
上面這個(gè)例子比較的簡(jiǎn)單,但大概就是這個(gè)意思。
關(guān)于CPython解釋器執(zhí)行源碼的過(guò)程可以參考官網(wǎng)描述:PEP 339
4.2 Closure 檢查
一個(gè)函數(shù)中定義的函數(shù)或者lambda中引用了父函數(shù)中的local variable,并且當(dāng)做返回值返回。特定場(chǎng)景下閉包是非常有用的,但是也很容易被誤用。
關(guān)于python閉包的概念可以參考我的另一篇文章:理解Python閉包概念
這里簡(jiǎn)單介紹一下如何借助ast來(lái)檢測(cè)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 閉包檢查原則: 只需檢測(cè)lambda expr body中args是否引用了lambda args list之外的參數(shù) """ 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可能非常復(fù)雜,上面的例子中僅僅處理了比較簡(jiǎn)單的body expr??筛鶕?jù)自己工程特點(diǎn)修改和擴(kuò)展檢查規(guī)則。為了更加一般化可以單獨(dú)寫(xiě)一個(gè)visitor類(lèi)來(lái)遍歷lambda節(jié)點(diǎn)。
Ast的應(yīng)用不僅限于上面的例子,限于篇幅,先介紹到這里。期待ast能幫助你解決一些比較棘手的問(wèn)題。
以上就是Python Ast抽象語(yǔ)法樹(shù)的介紹及應(yīng)用詳解的詳細(xì)內(nèi)容,更多關(guān)于Python Ast抽象語(yǔ)法樹(shù)的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
解決Django migrate No changes detected 不能創(chuàng)建表的問(wèn)題
今天小編就為大家分享一篇解決Django migrate No changes detected 不能創(chuàng)建表的問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2018-05-05python自動(dòng)化測(cè)試之異常及日志操作實(shí)例分析
這篇文章主要介紹了python自動(dòng)化測(cè)試之異常及日志操作,結(jié)合實(shí)例形式分析了python自動(dòng)化測(cè)試中的異常捕獲與日志記錄相關(guān)操作技巧,需要的朋友可以參考下2019-11-11python 如何用urllib與服務(wù)端交互(發(fā)送和接收數(shù)據(jù))
這篇文章主要介紹了python 如何用urllib與服務(wù)端交互(發(fā)送和接收數(shù)據(jù)),幫助大家更好的理解和學(xué)習(xí)使用python,感興趣的朋友可以了解下2021-03-03Python 調(diào)用C++封裝的進(jìn)一步探索交流
這篇文章主要介紹了Python 調(diào)用C++封裝的進(jìn)一步探索交流,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2021-03-03基于Python編寫(xiě)詞云軟件并顯示分詞結(jié)果
這篇文章主要為大家詳細(xì)介紹了如何基于Python編寫(xiě)一個(gè)簡(jiǎn)單的詞云制作軟件并顯示分詞結(jié)果,文中的示例代碼講解詳細(xì),具有一定的學(xué)習(xí)價(jià)值,感興趣的小伙伴可以了解一下2023-10-10Python imutils 填充圖片周邊為黑色的實(shí)現(xiàn)
今天小編就為大家分享一篇Python imutils 填充圖片周邊為黑色的實(shí)現(xiàn),具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2020-01-01詳解運(yùn)行Python的神器Jupyter Notebook
如果我們想要運(yùn)行Python,就是在Python或者IPython的解釋器環(huán)境中進(jìn)行交互式運(yùn)行,或者程序員最喜歡的編寫(xiě).py文件,在文件中編寫(xiě)python代碼,然后運(yùn)行。如果想寫(xiě)一篇Python的文章,里面有代碼,還希望代碼在當(dāng)前頁(yè)面運(yùn)行,那就是使用我們今天要介紹的Jupyter Notebook。2021-06-06Python使用defaultdict解決字典默認(rèn)值
本文主要介紹了Python使用defaultdict解決字典默認(rèn)值,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2023-04-04