python?ast模塊詳析與用法
基本概念
在 python 中,我們可以通過(guò)自帶的 ast
模塊來(lái)對(duì)解析遍歷語(yǔ)法樹(shù),通過(guò)ast.parse()
可以將字符串代碼解析為抽象語(yǔ)法樹(shù),然后通過(guò)ast.dump()
可以打印這棵語(yǔ)法樹(shù)。
除了ast
模塊外,還有 astor
模塊,其中的 astor.to_sourse()
函數(shù)可以將語(yǔ)法樹(shù)Node
轉(zhuǎn)換為代碼, astor.dump_tree()
可以很好地格式化整棵樹(shù)。
除了這些基礎(chǔ)操作外,我們還可以遍歷和修改整棵語(yǔ)法樹(shù)。
比如,對(duì)于a = 10
來(lái)說(shuō),我們可以先解析成抽象語(yǔ)法樹(shù),然后打印所有的結(jié)點(diǎn),如下所示。根據(jù)輸出,我們可以看到根節(jié)點(diǎn)是Module
類型的,然后其body
是Assign
類型的。對(duì)于Assign
類型的結(jié)點(diǎn),可以繼續(xù)劃分為Name
結(jié)點(diǎn)(表示變量名)和Constant
結(jié)點(diǎn)(表示變量?jī)?nèi)容)。
node = ast.parse('a = 10') print(astor.dump_tree(node)) # Module(body=[Assign(targets=[Name(id='a')], value=Constant(value=10, kind=None), type_comment=None)], type_ignores=[])
節(jié)點(diǎn)類型
上面的簡(jiǎn)單示例向我們展示了幾種基本結(jié)點(diǎn)類型(Assign、Name、Constant
),接下來(lái)我們將會(huì)展示其他幾種常見(jiàn)的結(jié)點(diǎn)類型和示例,完整的節(jié)點(diǎn)類型可以查閱節(jié)點(diǎn)類型。大體上,我們可以把結(jié)點(diǎn)類型分為葉子結(jié)點(diǎn)
類型和非葉子結(jié)點(diǎn)
類型,比如Assign
就是非葉子結(jié)點(diǎn)類型,Name
和Constant
是葉子結(jié)點(diǎn)類型,因?yàn)樗麄儾粫?huì)有子結(jié)點(diǎn)了。
ast.Assign
Assign
類型用來(lái)表示賦值語(yǔ)句,比如a = 10
、 b = a
這樣的賦值語(yǔ)句都是Assign
結(jié)點(diǎn)類型,他并不是一個(gè)葉子結(jié)點(diǎn),因?yàn)樗南旅嬉话氵€有 Name
結(jié)點(diǎn)。
ast.Name
Name
類型用來(lái)表示一個(gè)變量的名稱,是一個(gè)葉子結(jié)點(diǎn)
。比如對(duì)于b = a
這樣的賦值語(yǔ)句,子結(jié)點(diǎn)就是兩個(gè)Name
。
node = ast.parse('a = b') print(astor.dump_tree(node.body[0])) # Assign(targets=[Name(id='a')], value=Name(id='b'), type_comment=None)
ast.Constant
表示一個(gè)不可變內(nèi)容,它可以是Number
、string
,只要其內(nèi)容是不可變的,都是ast.Constant
類型的結(jié)點(diǎn),它是一個(gè)葉子結(jié)點(diǎn)
。
node = ast.parse('a = 100') print(astor.dump_tree(node.body[0])) # Assign(targets=[Name(id='a')], value=Constant(value=100, kind=None), type_comment=None) node = ast.parse('a = "paddle"') print(astor.dump_tree(node.body[0])) # Assign(targets=[Name(id='a')], value=Constant(value='paddle', kind=None), type_comment=None)
ast.Call
表示函數(shù)的調(diào)用,比如paddle.to_tensor()
。非葉子節(jié)點(diǎn)類型,一般包含三個(gè)屬性:func、args、 keywords
。
- func:代表調(diào)用函數(shù)的名稱,一般是一個(gè)
ast.Name
或ast.Constant
類型的結(jié)點(diǎn),如果是連續(xù)調(diào)用,會(huì)是一個(gè)ast.Call
結(jié)點(diǎn)。 - args:代表函數(shù)傳入的位置參數(shù)和可變參數(shù)。
- keywords:代表函數(shù)傳入的關(guān)鍵字參數(shù)。
node = ast.parse('paddle.to_tensor(1, a = 10)') print(astor.dump_tree(node.body[0])) # Expr( value=Call(func=Attribute(value=Name(id='paddle'), attr='to_tensor'), args=[Constant(value=1, kind=None)], keywords=[keyword(arg='a', value=Constant(value=10, kind=None))]))
對(duì)于上面的例子,我們通過(guò)可視化可以看到,頂層是一個(gè)ast.Expr
類型的結(jié)點(diǎn),表示一個(gè)表達(dá)式。下面是ast.Call 結(jié)點(diǎn)
,Call
結(jié)點(diǎn)包含 一個(gè)ast.Attribute
結(jié)點(diǎn),表示調(diào)用者和調(diào)用的方法名,paddle
是調(diào)用者,to_tensor
是方法名;一個(gè)ast.Constant
類型的args
,表示函數(shù)的位置參數(shù);一個(gè)ast.keyword
,表示函數(shù)的關(guān)鍵字參數(shù)。
下面我們看一個(gè)比較復(fù)雜的示例,多個(gè)函數(shù)的連續(xù)調(diào)用。根據(jù)輸出結(jié)果可以看到,最后的調(diào)用reshape
在最外層,然后一直向內(nèi)遞歸,子結(jié)點(diǎn)還是ast.Call
類型的結(jié)點(diǎn)。
node = ast.parse('a.to_tensor(1, a = 10).reshape(1)') print(astor.dump_tree(node.body[0])) Expr( value=Call( func=Attribute( value=Call(func=Attribute(value=Name(id='a'), attr='to_tensor'), args=[Constant(value=1, kind=None)], keywords=[keyword(arg='a', value=Constant(value=10, kind=None))]), attr='reshape'), args=[Constant(value=1, kind=None)], keywords=[]))
ast.Attribute
上面的例子中出現(xiàn)了ast.Attribute
結(jié)點(diǎn),Attribute
結(jié)點(diǎn)可以理解為屬性,是一個(gè)非葉子結(jié)點(diǎn)。它包含兩個(gè)字段,value
字段和attr
字段。對(duì)于a.shape
來(lái)說(shuō)value
指明調(diào)用者,即a
;attr
指明調(diào)用的方法名,即shape
。
node = ast.parse('a.shape') print(astor.dump_tree(node.body[0])) Expr(value=Attribute(value=Name(id='a'), attr='shape'))
結(jié)點(diǎn)的遍歷
在ast
模塊中,可以借助繼承ast.NodeVisitor
類來(lái)完成結(jié)點(diǎn)的遍歷,該類具有兩種訪問(wèn)結(jié)點(diǎn)的方法,一種是針對(duì)所有結(jié)點(diǎn)類型通用的訪問(wèn)方法generic_visit()
,另一種是針對(duì)某個(gè)類型結(jié)點(diǎn)的訪問(wèn)方法 visit_xxx
,其中xxx代表具體的結(jié)點(diǎn)類型。generic_visit()
函數(shù)是遍歷每個(gè)結(jié)點(diǎn)的入口函數(shù),隨后會(huì)調(diào)用visitor()
函數(shù),獲取該結(jié)點(diǎn)的類型,然后判斷是否有遍歷該類型結(jié)點(diǎn)的函數(shù),如果有則調(diào)用 visit_xxx
類型的方法,如果沒(méi)有則調(diào)用通用generic_visit()
方法。
ast源碼
class NodeVisitor(object): def visit(self, node): """Visit a node.""" method = 'visit_' + node.__class__.__name__ visitor = getattr(self, method, self.generic_visit) return visitor(node) def generic_visit(self, node): # 可以看到 generic_visit函數(shù)會(huì)調(diào)用visit函數(shù),然后尋找并調(diào)用特定類型的visit函數(shù)。 """Called if no explicit visitor function exists for a node.""" for field, value in iter_fields(node): if isinstance(value, list): for item in value: if isinstance(item, AST): self.visit(item) elif isinstance(value, AST): self.visit(value) def visit_Constant(self, node): value = node.value type_name = _const_node_type_names.get(type(value)) if type_name is None: for cls, name in _const_node_type_names.items(): if isinstance(value, cls): type_name = name break if type_name is not None: method = 'visit_' + type_name try: visitor = getattr(self, method) except AttributeError: pass else: import warnings warnings.warn(f"{method} is deprecated; add visit_Constant", PendingDeprecationWarning, 2) return visitor(node) return self.generic_visit(node)
示例
下面是一個(gè)例子,我們定義了一個(gè)繼承ast.NodeVisitor
的類,并且重寫(xiě)了visit_attribute
方法,這樣在遍歷到ast.Attribute
結(jié)點(diǎn)時(shí),會(huì)輸出當(dāng)前調(diào)用的屬性名或方法名,對(duì)于其他類型的結(jié)點(diǎn)則會(huì)輸出結(jié)點(diǎn)類型。
class CustomVisitor(ast.NodeVisitor): def visit_Attribute(self, node): print('----' + node.attr) ast.NodeVisitor.generic_visit(self, node) def generic_visit(self, node): print(node.__class__.__name__) ast.NodeVisitor.generic_visit(self, node) code = textwrap.dedent( ''' import paddle x = paddle.to_tensor([1, 2, 3]) axis = 0 y = paddle.max(x, axis=axis) ''' ) node = ast.parse(code) visitor = CustomVisitor() visitor.generic_visit(node)
需要注意的是,當(dāng)我們重寫(xiě)visit_xxx函數(shù)后,一定要記得再次調(diào)用
ast.NodeVisitor.generic_visit(self, node)
,這樣才會(huì)繼續(xù)遍歷整棵語(yǔ)法樹(shù)。
結(jié)點(diǎn)的修改
對(duì)于結(jié)點(diǎn)的修改可以借助ast.NodeTransformer
類來(lái)完成,ast.NodeTransformer
繼承自ast.NodeVisitor
類,重寫(xiě)了generic_visit
方法,該方法可以傳入一個(gè)結(jié)點(diǎn),并且返回修改后的結(jié)點(diǎn),從而完成語(yǔ)法樹(shù)的修改。
示例
在該示例中,我們定義了CustomVisitor
類來(lái)修改ast.Call
結(jié)點(diǎn)。具體來(lái)說(shuō),當(dāng)遍歷到Call
類型的結(jié)點(diǎn)后,流程如下:
- 首先會(huì)調(diào)用get_full_attr方法獲取整個(gè)api名稱,如果是普通方法調(diào)用,則會(huì)返回完整的調(diào)用名稱,比如torch.tensor()會(huì)返回torch.tensor;如果是連續(xù)的方法調(diào)用,比如x.exp().floor(),則會(huì)返回ClassMethod.floor。
- 然后調(diào)用 ast.NodeVisitor.generic_visit(self, node) ,進(jìn)行深度優(yōu)先的修改,這樣就可以一層層遞歸,先修改內(nèi)層,再修改外層。
- 如果是普通的方法調(diào)用,則修改結(jié)點(diǎn)后返回;
- 如果是連續(xù)的方法調(diào)用,需要先通過(guò)astor.to_source(node)獲取前綴方法,即調(diào)用者,保留前綴方法名稱的同時(shí),修改目前的方法名后返回。具體是通過(guò)'{}.{}()'實(shí)現(xiàn)的。
def get_full_attr(node): # torch.nn.fucntional.relu if isinstance(node, ast.Attribute): return get_full_attr(node.value) + '.' + node.attr # x.abs() -> 'x' elif isinstance(node, ast.Name): return node.id # for example ast.Call else: return 'ClassMethod' class CustomVisitor(ast.NodeTransformer): def visit_Call(self, node): # 獲取api的全稱 full_func = get_full_attr(node.func) # post order ast.NodeVisitor.generic_visit(self, node) # 如果是普通方法調(diào)用,直接改寫(xiě)整個(gè)結(jié)點(diǎn)即可 if full_func == 'torch.tensor': # 將 torch.tensor() 改寫(xiě)為 paddle.to_tensor() code = 'paddle.to_tensor()' new_node = ast.parse(code).body[0] return new_node.value # 如果是類方法調(diào)用,需要取前面改寫(xiě)后的方法作為 func.value if full_func == 'ClassMethod.floor': # 獲取前綴方法作為 func.value new_func = astor.to_source(node).strip('\n') new_func = new_func[0: new_func.rfind('.')] # 將 floor() 改寫(xiě)為 floor2() code = '{}.{}()'.format(new_func, 'floor2') new_node = ast.parse(code).body[0] return new_node.value # 其余結(jié)點(diǎn)不修改 return node code = textwrap.dedent( ''' import torch x = torch.tensor([1, 2, 3]) x = x.exp().floor() ''' ) node = ast.parse(code) visitor = CustomVisitor() node = visitor.generic_visit(node) result_code = astor.to_source(node) print(result_code)
參考鏈接
- https://blog.csdn.net/ThinkTimes/article/details/110831176?ydreferer=aHR0cHM6Ly9jbi5iaW5nLmNvbS8%3D
- https://greentreesnakes.readthedocs.io/en/latest/
- https://github.com/PaddlePaddle/PaConvert
總結(jié)
到此這篇關(guān)于python ast模塊詳析與用法的文章就介紹到這了,更多相關(guān)python ast模塊內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
python 統(tǒng)計(jì)數(shù)組中元素出現(xiàn)次數(shù)并進(jìn)行排序的實(shí)例
今天小編就為大家分享一篇python 統(tǒng)計(jì)數(shù)組中元素出現(xiàn)次數(shù)并進(jìn)行排序的實(shí)例,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2018-07-07python中List添加與刪除元素的幾種方法實(shí)例
列表基本上是?Python?中最常用的數(shù)據(jù)結(jié)構(gòu)之一了,并且刪除操作也是經(jīng)常使用的,下面這篇文章主要給大家介紹了關(guān)于python中List添加與刪除元素的相關(guān)資料,需要的朋友可以參考下2022-09-09Python進(jìn)階-函數(shù)默認(rèn)參數(shù)(詳解)
下面小編就為大家?guī)?lái)一篇Python進(jìn)階-函數(shù)默認(rèn)參數(shù)(詳解)。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-05-05python基于tkinter圖形化編程實(shí)現(xiàn)簡(jiǎn)易計(jì)算器功能
這篇文章主要為大家詳細(xì)介紹了python基于tkinter圖形化編程實(shí)現(xiàn)簡(jiǎn)易計(jì)算器功能,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-07-07Python源碼學(xué)習(xí)之PyType_Type和PyBaseObject_Type詳解
今天給大家?guī)?lái)的是關(guān)于Python源碼的相關(guān)知識(shí)學(xué)習(xí),文章圍繞著PyType_Type和PyBaseObject_Type展開(kāi),文中有非常詳細(xì)的介紹及代碼示例,需要的朋友可以參考下2021-06-06python實(shí)現(xiàn)FTP文件傳輸?shù)姆椒ǎǚ?wù)器端和客戶端)
FTP(File Transfer Protocol,文件傳輸協(xié)議) 是 TCP/IP 協(xié)議組中的協(xié)議之一。接下來(lái)通過(guò)本文給大家介紹關(guān)于python實(shí)現(xiàn)FTP文件傳輸?shù)南嚓P(guān)知識(shí)(服務(wù)器端和客戶端) ,需要的朋友可以參考下2020-03-03關(guān)于Python自動(dòng)化操作Excel
這篇文章主要介紹了關(guān)于Python自動(dòng)化操作Excel, Python 是一種功能強(qiáng)大的編程語(yǔ)言,可以用于許多任務(wù),包括處理 Excel 文件,需要的朋友可以參考下2023-04-04scipy稀疏數(shù)組coo_array的實(shí)現(xiàn)
本文主要介紹了scipy稀疏數(shù)組coo_array的實(shí)現(xiàn),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2023-02-02