AST - 像lisp一樣自定義代碼行為
前言
學common lisp(以下除非特殊需要說明的都簡稱lisp)以及用emacs的人都有一個體會 - lisp無所不能, 可以使用lisp修改lisp的行為. 什么意思呢?
我來舉個例子. 我希望重置+的行為為實際意義的減法-. 看起來這是語言不可能完成的任務, 對lisp來說很簡潔(我使用sbcl):
* (+ 1 1)2 ; 正確結果
- (shadow '+)
T
- (defgeneric + (a &rest b))
<STANDARD-GENERIC-FUNCTION + (0)>
- (defmethod + ((a number) &rest b) (apply 'cl:- a b))
<STANDARD-METHOD + (NUMBER) {1002E43E73}>
- (+ 1 1)
0 ; 這里的加號的意義其實是我們所理解的減號
</pre>
是不是很神奇?
那么對于python這種高級語言能不能做到呢? 答案是肯定的. 我們馬上就來實現它
In [1]: import astIn [2]: x = ast.parse('1 + 1', mode='eval')
In [3]: x.body.op = ast.Sub()
In [4]: eval(compile(x, '<string>', 'eval')) Out[4]: 0</pre>
我想大家開始明白AST有多大能量了吧?
AST的故事
AST中文叫做抽象語法樹, 也就是分析當前版本的python代碼的語法, 用一種樹的結構解析出來. 這個模塊提供給我們一個在編譯代碼之前, 用python語言本身去修改.
它的作者是Armin Ronacher. 如果你聽過或者覺得似曾相識, 對. 他就是mitsuhiko - flask的作者. 也是pocoo的leader之一(另外一個是看起來不知名的birkenfeld - 對我來說他很有名).
那么AST有什么意義呢? 但是有絕大多數人其實不了解也用不到這個模塊, 為什么呢?
- 出現需要對代碼默認行為做更改的場景很少
- 它主要用來做靜態文件的檢查, 比如pylint, pychecker,以及寫flake8插件. 而我們平時的寫代碼都是在運行不需要進行預先的語法檢查之類, 那么實際接觸它就很難得了.
</ol>一些文章的索引
為了對本文有更深的理解可以看看以下文章
AST 模塊:用 Python 修改 Python 代碼這里對流程說的很好了. 可以直接讀一下
模塊代碼也寫得非常精煉, 可能不直接讓你明白, 那么這時候可以看看
Abstract Syntax Trees, 這個時候我再強調一下作者吧, takluyver是ipython的核心開發成員, 他也參與了很多我們常用的開源項目, 比如pexpect和pandas
上面的2篇文章寫了很多, 既有理解, 也有一些初級的用法.
我個人用它的例子
最近做的slack-alert. 先說它和AST的關系:
- 我沒有使用注冊或者import的方式,而是直接去遍歷文件, 找到符合我要求的函數當做一個任務需要執行的任務
- 任務就要設置間隔, 那么會加某種格式的裝飾器, 裝飾器的參數就是間隔類型, 比如@deco(seconds=10)表示沒十秒跑一次的意思
- 我這樣就可以放心的寫plugin就好了, 我只關注任務本身的邏輯. 而這個裝飾器(類似上面說的@deco), 它其實是不存在
- 這個特殊格式的裝飾器本身不存在沒有關系, 因為我不會直接運行代碼, 我只是把代碼通過AST的處理, 解析出我要的任務和任務的執行間隔. 再去編譯代碼.
</ol>上代碼:
class GetJobs(ast.NodeTransformer):def __init__(self): # 原來的ast.NodeTransformer其實沒有__init__方法 self.jobs = [] def get_jobs(self): # 一個方便的獲得任務的方法 return self.jobs def get_job_args(self, decorator): # 這屬于解析裝飾器這個結構, 拿到執行的間隔 return {k.arg: k.value.n for k in decorator.keywords if k.arg in ('hours', 'seconds', 'minutes', 'days') and isinstance(k.value, ast.Num)} def visit_FunctionDef(self, node): # 這個visit_xxx的方法被重載的時候, 就會對這個類型的語法加一些特殊處理. 因為我設計的時候只有函數才有可能是任務 decorator_list = node.decorator_list # 或者一個函數的裝飾器列表 if not decorator_list: return node # 沒有裝飾器明顯不是我想要的任務, 可能只是一個helper函數而已 decorator = decorator_list[0] # 這里我把最外面的裝飾器取出來看看是不是符合我要的格式 args = self.get_job_args(decorator) if args: # 當獲得了適合的參數, 那么正確這個格式是正確的 node.decorator_list = decorator_list[1:] # 最外面的裝飾器就是語法hack, 它不存在也沒有意義,以后完成歷史任務 去掉之 self.jobs.append((node.name, args)) return node
def find_jobs(path): jobs = [] for root, dirs, files in os.walk(path): for name in files: file = os.path.join(root, name) if not file.endswith('.py'): continue with open(file) as f: expr_ast = ast.parse(f.read()) # 讀文件, 解析 transformer = GetJobs() sandbox = {} # 其實就是把執行放在一個命名空間里面, 因為最后我還是會把任務編譯執行的, 我在這里面存了執行后的環境 exec(compile(transformer.visit(expr_ast), '<string>', 'exec'), sandbox) jobs.extend([(sandbox[j], kw) for j, kw in transformer.jobs]) return jobs</pre>
其實看起來不能完成的事情, 就是這么簡單.
來自:http://www.dongwm.com/archives/ast-xiang-lisp%5B%3F%5D-yang-zi-ding-yi-dai-ma-xing-wei/