AST - 像lisp一樣自定義代碼行為

jopen 9年前發布 | 18K 次閱讀 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 ast

In [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有什么意義呢? 但是有絕大多數人其實不了解也用不到這個模塊, 為什么呢?

  1. 出現需要對代碼默認行為做更改的場景很少
  2. 它主要用來做靜態文件的檢查, 比如pylint, pychecker,以及寫flake8插件. 而我們平時的寫代碼都是在運行不需要進行預先的語法檢查之類, 那么實際接觸它就很難得了.
  3. </ol>

    一些文章的索引

    為了對本文有更深的理解可以看看以下文章

    AST 模塊:用 Python 修改 Python 代碼這里對流程說的很好了. 可以直接讀一下

    模塊代碼也寫得非常精煉, 可能不直接讓你明白, 那么這時候可以看看

    Abstract Syntax Trees, 這個時候我再強調一下作者吧, takluyver是ipython的核心開發成員, 他也參與了很多我們常用的開源項目, 比如pexpect和pandas

    上面的2篇文章寫了很多, 既有理解, 也有一些初級的用法.

    我個人用它的例子

    最近做的slack-alert. 先說它和AST的關系:

    1. 我沒有使用注冊或者import的方式,而是直接去遍歷文件, 找到符合我要求的函數當做一個任務需要執行的任務
    2. 任務就要設置間隔, 那么會加某種格式的裝飾器, 裝飾器的參數就是間隔類型, 比如@deco(seconds=10)表示沒十秒跑一次的意思
    3. 我這樣就可以放心的寫plugin就好了, 我只關注任務本身的邏輯. 而這個裝飾器(類似上面說的@deco), 它其實是不存在
    4. 這個特殊格式的裝飾器本身不存在沒有關系, 因為我不會直接運行代碼, 我只是把代碼通過AST的處理, 解析出我要的任務和任務的執行間隔. 再去編譯代碼.
    5. </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/

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!