python模板引擎的基本原理
這兩天簡單研究了一下web.py框架,發現比較有意思的就是template模板引擎的實現了,所以簡單研究了一下基本原理。
例子
我們先看一個web.py的模板例子。
我有這樣一個模板檔案:
$def with (words) <!DOCTYPE html> <html lang="en"> <head> <title>web-py</title> $:render.header() </head> <body> <h1>$words</h1> <ul> $for i in range(0, 10): <li>$i</li> </ul> </body> </html>
這是一個HTML檔案,凡是動態內容,均需要以$開頭標識出來,以便模板引擎解析替換。
- $def with (words):這是定義了python要傳進來的動態引數,在<h1>$words</h1>中渲染。
- $:render.header():這是呼叫了render物件的header()方法,作用是渲染另外一個模板檔案。
- $for i in range(0,10):通過迴圈制作一個列表。
按照簡單理解,python只需要把模板檔案讀進記憶體,然後用正則去替換一下$words這樣的東西就好了。但是複雜就是,模板引擎還允許執行python程式碼,比如呼叫header()函式,for迴圈,這種用正則怎麼可能處理的了呢?所以,模板引擎的實現思路就特別重要了。
模板 -> 程式碼
如何讓模板引擎可以執行Python程式碼呢?我們不如先看看web.py是如何渲染的。
web.py對上述模板檔案經過一番處理,得到的實際上就是一段python程式碼:
# coding: utf-8 def __template__ (words): __lineoffset__ = -4 loop = ForLoop() self = TemplateResult(); extend_ = self.extend extend_(['\n']) extend_(['<!DOCTYPE html>\n']) extend_(['<html lang="en">\n']) extend_(['<head>\n']) extend_([' <title>web-py</title>\n']) extend_([' ', escape_(render.header(), False), '\n']) extend_(['</head>\n']) extend_(['<body>\n']) extend_([' <h1>', escape_(words, True), '</h1>\n']) extend_([' <ul>\n']) for i in loop.setup(range(0, 10)): extend_([' ', '<li>', escape_(i, True), '</li>\n']) extend_([' </ul>\n']) extend_(['</body>\n']) extend_(['</html>\n']) return self
觀察可以發現,這個函式就是HTML模板檔案的python實現:
- 模板的入參變成了__template__函式的入參
- 文字的HTML程式碼變成了python不斷extend_追加字串的過程
- 巢狀渲染其他模板變成了header()函式呼叫,結果又extend_到最終結果中去
- 迴圈也變成了真的python程式碼迴圈
實際上,模板引擎要做的就是對模板檔案進行語法解析,把普通文字轉換為extend_(xxx),把python表示式原樣的挪過來,最後拼成一個可執行的python函式。
當然,這個解析過程涉及到語法解析的知識,我個人不擅長,所以不展開研究了。
編譯程式碼
當前我們通過語法解析,生成了上述的一段python程式碼(是一段文字而已),這段程式碼做的唯一的事情就是定義了一個__template__函式。
現在我們要用這個函式來渲染模板得到最終的HTML,所以需要執行這段程式碼,得到真正的python可執行函式。
我把這個事情簡化一下,現在我們要做的事情是先編譯這段程式碼:
# -*- coding: utf-8 -*- # 純文字程式碼 code = """ def f(x): return x * x """ # 編譯文字程式碼, 生成AST抽象語法樹 ast = compile(code, '', 'exec')
code中的文字程式碼,被編譯成了AST抽象語法表達樹,但是當前還沒有被執行。
執行程式碼
如果code中的程式碼放在一個.py檔案中,我們知道直接呼叫f()就可以了,但是現在code只是一段字串,這時候如何執行程式碼呢?
這時候我們需要用到exec方法,它可以動態執行一段程式碼,同時允許單獨指定程式碼執行的上下文環境:
# 執行程式碼 global_env = {} exec(ast, global_env)
exec第一個引數是程式碼,第二個引數是所處的全域性環境,第三個程式碼是所處的區域性環境。
我們知道,當在全域性作用域定義函式的時候,函式會被儲存到globals()中;在區域性作用域(比如某個函式內)定義函式的時候,函式會被儲存到locals()中。
所以我們定義一個空字典global_env當做程式碼執行的全域性環境,那麼f函式就會儲存到global_env中。
呼叫函式
現在,我們可以從global_env中取出f函式,直接呼叫它即可:
# 函式f就被定義到global_env中了, 我們可以取出來可執行函式 f = global_env['f'] # 呼叫它 result = f(5) print(result)
這就是模板引擎的基本工作原理了,所以模板引擎一般都會把編譯好的__template__函式cache起來複用,因為如果每次都去解析模板檔案再生成可執行函式的消耗有點大。