關於簡介和模板問題請在參考文檔查看python
1.模板的編寫:https://blog.csdn.net/MageeLen/article/details/68920913正則表達式
模板引擎的核心就是這個Templite類(Template Lite)express
Templite有一個小的接口。一旦你構造了這樣一個類,後面就能夠經過調用render
方法實現對特定context
(內容字典)的渲染:閉包
# Make a Templite object. templite = Templite(''' <h1>Hello {{name|upper}}!</h1> {% for topic in topics %} <p>You are interested in {{topic}}.</p> {% endfor %} ''', {'upper': str.upper}, ) # Later, use it to render some data. text = templite.render({ 'name': "Ned", 'topics': ['Python', 'Geometry', 'Juggling'], })
這裏,咱們在例化的時候已經將模板傳入,以後咱們就能夠直接對模板進行一次編譯,在以後就能夠經過render
方法對模板進行屢次調用。app
構造函數接受一個字典參數做爲內容的初始化,他們直接被存儲在類內部,在後期調用render
方法的時候能夠直接引用。一樣,一些會用到的函數或常量也能夠在這裏輸入,好比以前的upper
函數。函數
再開始討論Temlite類實現以前,咱們先來看一下這樣一個類:CodeBuilder。工具
咱們編寫模板引擎的主要工做就是模板解析和產生必要的Python代碼。爲了幫助咱們更好的產生Python代碼,咱們須要一個CodeBuilder
的類,這個類主要負責代碼的生成:添加代碼,管理縮進以及返回最後的編譯結果。oop
一個CodeBuilder
實例完成一個Python方法的構建,雖然在咱們模板引擎中只須要一個函數,可是爲了更好的抽象,下降模塊耦合,咱們的CodeBuilder
將不只僅侷限於生成一個函數。測試
雖然咱們可能直到最後纔會知道咱們的結果是什麼樣子,咱們仍是把這部分拿到前面來講一下。優化
CodeBuilder
主要有兩個元素,一個是用於保存代碼的字符串列表,另一個是標示當前的縮進級別。
class CodeBuilder(object): """Build source code conveniently.""" def __init__(self, indent=0): self.code = [] self.indent_level = indent
下面咱們來看一下咱們須要的接口和具體實現。
add_line
方法將添加一個新的代碼行,縮進將自動添加
indent
和dedent
增長和減小縮進級別的函數:
INDENT_STEP = 4 def indent(self): """Increase the current indent for following lines.""" self.indent_level += self.INDENT_STEP def dedent(self): """Decrease the current indent for following lines.""" self.indent_level -= self.INDENT_STEP
add_section
經過另外一個CodeBuilder管理,這裏先預留一個位置,後面再繼續完善,self.code
主要由代碼字符列表構成,但同時也支持對其餘代碼塊的引用。
def add_section(self): """Add a secton, a sub-CodeBuilder.""" section = CodeBuilder(self.indent_level) self.code.append(section) return section
__str__
用於產生全部代碼,它將遍歷self.code
列表,而對於self.code
中的sections,它也會進行遞歸調用:
def __str__(self): return ''.join(str(c) for c in self.code)
get_globals
經過執行代碼迭代生成結果:
def get_globals(self): """Executer the code, and return a dict of globals if defnes.""" # A check that caller really finished all the blocks assert self.indent_level == 0 # Get the Python source as a single string python_source = str(self) # Execute the source, defining globals, and return them. global_namespace = {} exec(python_source, global_namespace) return global_namespace
在這裏面用到了Python一個很是有特點的功能,exec
函數,該函數能夠將字符串做爲代碼執行,函數的第二個參數是一個用於收集代碼定義全局變量的一個字典,好比:
python_source = """\ SEVENTEEN = 17 def three(): return 3 """ global_namespace = {} exec(python_source, global_namespace) print(global_namespace['SEVENTEEN'], global_namespace['three'])
輸出結果:
(17, <function three at 0x029FABB0>) [Finished in 0.1s]
雖然咱們只須要CodeBuilder產生一個函數,可是實際CodeBuilder的使用並不侷限於一個函數,它實際是一個更爲通用的類。
CodeBuilder能夠產生Python代碼,可是並不依賴於咱們的模板,好比咱們要產生三個函數,那麼get_global
實際就能夠產生含有三個函數的字典,這是一種很是實用的程序設計方法。
下面咱們迴歸Templite類,看一下如何去實現這樣一個類
就像以前咱們所講的同樣,咱們的主要任務在於實現模板發解析和渲染。
這部分工做須要完成模板代碼到python代碼的轉換,咱們先嚐試寫一下構造器:
def __init__(self, text, *contexts): """Construct a Templite with the given 'text'. 'contexts' are dictionaries of values to future renderings. These are good for filters and global values. """ super(Templite, self).__init__() self.context = {} for context in contexts: self.context.update(context)
注意,咱們使用*contexts
做爲一個參數, *
表明能夠傳入任意數量的參數,全部的參數都將打包在一個元組裏面,元組名稱爲contexts
。這稱之爲參數解包,好比咱們能夠經過以下方式進行調用:
t = Templite(template_text) t = Templite(template_text, context1) t = Templite(template_text, context1, context2)
內容參數做爲一個元組傳入,咱們經過對元組進行遍歷,對其依次進行處理,在構造器中咱們聲明瞭一個self.context
的字典, python中對重名狀況直接使用最近的定義。
一樣,爲了更有效的編譯函數,咱們將context
中的變量也本地化了,咱們一樣還須要對模板中的變量進行整理,因而咱們定義以下兩個元素:
self.all_vars = set() self.loop_vars = set()
以後咱們會講到如何去運用這些變量。首先,咱們須要用CodeBuilder類去產生咱們編譯函數的定義:
code = CodeBuilder() code.add_line("def render_function(context, do_dots):") code.indent() vars_code = code.add_section() code.add_line("result = []") code.add_line("append_result = result.append") code.add_line("extend_result = result.extend") code.add_line("to_str = str")
這裏,咱們構造一個CodeBuilder類,添加函數名稱爲render_function
,以及函數的兩個參數:數據字典context
和實現點號屬性獲取的函數do_dots
這裏的數據字典包括傳入Templite例化的數據字典和用於渲染的數據字典。是整個能夠獲取的數據的一個集合。
而做爲代碼生成工具的CodeBuilder並不關心本身內部是什麼代碼,這樣的設計使CodeBuilder更爲簡潔和易於實現。
咱們還建立了一個名稱爲vars_code
的代碼段,後面咱們會把咱們的變量放到這個段裏面,該代碼段爲咱們預留了一個後面添加代碼的空間。
另外的四行分別添加告終果列表result
的定義,局部函數的定義,正如以前說過的,這都是爲了提高運行效率而添加的變量。
接下來,咱們定義一個用於緩衝輸出的內部函數:
buffered = [] def flush_output(): """ Force 'buffered' to the code builder.""" if len(buffered) == 1: code.add_line("append_result(%s)" % buffered[0]) elif len(buffered) > 1: code.add_line("extend_result([%s])" % ", ".join(buffered)) del buffered[:]
由於咱們須要添加不少code到CodeBuilder,因此咱們選擇將這種重複的添加合併到一個擴展函數,這是另外的一種優化,爲了實現這種優化,咱們添加一個緩衝函數。
buffered
函數保存咱們將要寫入的code,而在咱們處理模板的時候,咱們會往buffered
列表裏添加字符串,直到遇到其餘要處理的點,咱們再將緩衝的字符寫入生成函數,要處理的點包括代碼段,或者循環判斷語句的開始等標誌。
flush_output
函數是一個閉包,裏面的變量包括buffered
和code
。這樣咱們之後調用的時候就不須要指定寫入那個code,從那個變量讀取數據了。
在函數裏,若是隻是一個字符串,那麼調用append_result函數,若是是字符串列表,則調用extend_result函數。
擁有這個函數以後,後面須要添加代碼的時候只須要往buffered
裏面添加就能夠了,最後調用一次flush_ouput
便可完成代碼到CodeBuilder中的添加。
好比咱們有一行代碼須要添加,便可採用下面的形式:
buffered.append("'hello'")
後面會添加以下代碼到CodeBuilder
append_result('hello')
也就是將字符串hello
添加到模板的渲染。太多層的抽象實際很難保持一致性。編譯器使用buffered.append("'hello'"), 這將生成
append_result(‘hello’)「到編譯結果中。
讓咱們再回到Templite類,在咱們進行解析的時候,咱們須要判斷模板
可以正確的嵌套,這就須要一個ops_stack
來保存字符串堆棧:
ops_stack = []
好比在遇到{% if ... %}
標籤的時候,咱們就須要將’if’進行壓棧,當遇到{% endif %}
的時候,須要將以前的的’if’出棧,若是解析完模板的時候,棧內還有數據,就說明模板沒有正確的使用。
如今開始作解析模塊。首先經過使用正則表達式將模板文本進行分組。正則表達式是比較煩人的: 正則表達式主要經過簡單的符號完成對字符串的模式匹配。由於正則表達式的執行是經過C完成的,所以有很高的效率,可是最初接觸時比較複雜難懂,好比:
tokens = re.split(r"(?s)({{.*?}}|{%.*?%}|{#.*?#})", text)
看起來是否是至關複雜?咱們來簡單解釋一下:
re.split
函數主要經過正則表達式完成對字符串的分組,而咱們的正則表達式內部也含有分組信息(()
),所以函數將返回對字符串分組後的結果,這裏的正則主要匹配語法標籤,因此最終字符串將在還有語法標籤的地方被分割,而且相應的語法標籤也會被返回。
正則表達式裏的(?s)
表示即便在一個新行也須要有一個點號(?),後面的分組有三種不一樣的選項:{{.*?
會匹配一個標籤,{%.*?%}
會匹配一個語句表達式,{#.*?#}
會匹配一個註釋。這幾個選項裏面,咱們用.*?
來匹配任意數目的任意字符,不過用了非貪婪匹配,所以它將只匹配最少數目的字符。
re.split
的輸出結果是一個字符串列表,若是模板是以下的字符:
<p>Topics for {{name}}: {% for t in topics %}{{t}}, {% endfor %}</p>
將會返回以下的結果
[ '<p>Topics for ', # literal '{{name}}', # expression ': ', # literal '{% for t in topics %}', # tag '', # literal (empty) '{{t}}', # expression ', ', # literal '{% endfor %}', # tag '</p>' # literal ]
一旦將模板進行了分組,咱們就能夠對結果進行遍歷,對每種不一樣的類型進行不一樣的處理。
好比對各類符號的編譯能夠採用以下的形式:
for token in tokens:
在遍歷的時候,咱們須要判斷每一個標誌的類型,實際咱們只須要判斷前兩個字符。而對於註釋的標誌處理最爲簡單,咱們只須要簡單的跳過便可:
if token.startwith('{#'): # Comment: ignore it and move on. continue
對於{{ ... }}
這樣的表達式,須要將兩邊的括號刪除,刪減表達式兩邊的空格,最後將表達式傳入到_expr_code
:
elif token.startwith("{{"): # An expression to evalute. expr = self._expr_code(token[2:-2].strip()) buffered.append("to_str(%s)" % expr)
_expr_code
方法會將模板中的表達式編譯成Python語句,後面會具體降到這個方法的實現。再以後經過to_str
函數將編譯後的表達式轉換爲字符串添加到咱們的結果中。
後面一個條件判斷最爲複雜:{% ... %}
語法標籤的處理。它們將會被編譯成Python中的代碼段。在操做以前,首先須要將以前的結果保存,以後須要從標籤中抽取必要的關鍵詞進行處理:
elif token.startwith("{%"): # Action tag: split into words and parse futher flush_output() words = token[2:-2].strip().split()
目前支持的語法標籤主要包含三種結構:if
, for
和end
. 咱們來看看對於if
的處理:
if words[0] == 'if': # An if statement: evalute the expression to determine if. if len(words) != 2: self._syntax_error("Don't understand if", token) ops_stack.append('if') code.add_line("if %s:" % self._expr_code(words[1])) code.indent()
這裏if
後面必須有一個表達式,所以words
的長度應該爲2(譯者:難道不會有空格??),若是長度不正確,那麼將會產生一個語法錯誤。以後會對if
語句進行壓棧處理以便後面檢測是否有相應的endif
結束標籤。if
後面的判斷語句經過_expr_code
編譯,並添加if
代碼後添加到結果,最後增長一級縮進。
第二種標籤類型是for
, 它將被編譯爲Python的for語句:
elif word[0] == 'for': # A loop: iterate over expression result. if len(words) != 4 or words[2] != 'in': self._syntax_error("Don't understand for", token) ops_stack.append('for') self._veriable(words[1], self.loop_vars) code.add_line( "for c_%s in %s:" % ( words[1], self._expr_code(words[3])) ) code.indent()
這一步咱們檢查了模板的語法,而且將for
標籤壓棧。_variable
方法主要檢測變量的語法,並將變量加入咱們的變量集。咱們經過這種方式來實現編譯過程當中變量的統計。後面咱們會對函數作一個統計,並將變量集合添加在裏面。爲實現這一操做,咱們須要將遇到的全部變量添加到self.all_vars
,而對於循環中定義的變量,須要添加到self.loop_vars
.
在這以後,咱們添加了一個for
代碼段。而模板中的變量經過加c_
前綴被轉化爲python中的變量,這樣能夠防止模板中變量與之衝突。經過使用_expr_code
將模板中的表達式編譯成Python中的表達式。
最後咱們還須要處理end
標籤;實際對{% endif %}
和{% endfor %}
來講都是同樣的:主要完成對相應代碼段的減小縮進功能。
elif word[0].startwith('end'): #Endsomting. pop the ops stack. if len(words) != 1: self._syntax_error("Don't understand end", token) end_what = words[0][3:] if not ops_stack: self._syntax_error("Too many engs", token) start_what = ops_stack.pop() if start_what ~= end_what: self._syntax_error("Mismatched end tag", end_what) code.dedent()
注意,這裏結束標籤最重要的功能就是結束函數代碼塊,減小縮進。其餘的都是一些語法檢查,這種操做在翻譯模式通常都是沒有的。
說到錯誤處理,若是標籤不是if
, for
或者end
,那麼程序就沒法處理,應該拋出一個異常:
else: self._syntax_error("Don't understand tag", word[0])
在處理完三種不一樣的特殊標籤{{ ... }}
, {# ... #}
和{% ... %}
以後。剩下的應該就是普通的文本內容。咱們須要將這些文本添加到緩衝輸出,經過repr
方法將其轉換爲Python中的字符串:
else: #literal content, if not empty, output it if token: buffered.append(repr(token))
若是不使用repr
方法,那麼在編譯的結果中就會變成:
append_result(abc) # Error! abc isn't defined
相應的咱們須要以下的形式:
append_result('abc')
repr
函數會自動給引用的文本添加引號,另外還會添加必要的轉意符號:
append_result('"Don\'t you like my hat?" he asked.')
另外咱們首先檢測了字符是否爲空if token:
, 由於咱們不必將空字符也添加到輸出。空的tokens
通常出如今兩個特殊的語法符號中間,這裏的空字符檢測能夠避免向最終的結果添加append_result("")
這樣沒有用的代碼。
上面的代碼基本完成了對模板中語法標籤的遍歷處理。當遍歷結束時,模板中全部的代碼都被處理。在最後,咱們還須要進行一個檢測:若是ops_stack
非空,說明模板中有未閉合的標籤。最後咱們再將全部的結果寫入編譯結果。
if ops_stack: self._syntax_error("Unmatched action tag", ops_stack[-1]) flush_output()
還記得嗎,咱們在最開始建立了一個代碼段。它的做用是爲了將模板中的代碼抽取並轉換到Python本地變量。 如今咱們對整個模板都已經遍歷處理,咱們也獲得了模板中全部的變量,所以咱們能夠開始着手處理這些變量。
在這以前,咱們來看看咱們須要處理變量名。先看看咱們以前定義的模板:
<p>Welcome, {{user_name}}!</p> <p>Products:</p> <ul> {% for product in product_list %} <li>{{ product.name }}: {{ product.price|format_price }}</li> {% endfor %} </ul>
這裏面有兩個變量user_name
和product
。這些變量在模板遍歷後都會放到all_vars
集合中。可是在這裏咱們只須要對user_name
進行處理,由於product
是在for循環中定義的。
all_vars
存儲了模板中的全部變量,而loop_vars
則存儲了循環中的變量,由於循環中的變量會在循環的時候進行定義,所以咱們這裏只須要定義在all_vars
卻不在loop_vars
的變量:
for var_name in self.all_vars - self.loop_vars: vars_code.add_line("c_%s = context[%r]" % (var_name, var_name))
這裏每個變量都會從context
數據字典中得到相應的值。
如今咱們基本上已經完成了對模板的編譯。最後咱們還須要將函數結果添加到result
列表中,所以最後還須要添加以下代碼到咱們的代碼生成器:
code.add_line("return ''.join(result)") code.dedent()
到這裏咱們已經實現了對模板到python代碼的編譯,編譯結果須要從代碼生成器CodeBuilder
中得到。能夠經過get_globals
方法直接返回。還記得嗎,咱們須要的代碼只是一個函數(函數以def render_function():
開頭), 所以編譯結果是獲得這樣一個render_function
函數而不是函數的執行結果。
get_globals
的返回結果是一個字典,咱們從中取出render_function
函數,並將它保存爲Templite
類的一個方法。
self._render_function = code.get_globals()['render_function']
如今self._render_function
已是一個能夠調用的Python函數,咱們後面渲染模板的時候會用到這個函數。
到如今咱們還不能看到實際的編譯結果,由於有個一重要的方法_expr_code
尚未實現。這個方法能夠將模板中的表達式編譯成python中的表達式。有時候模板中的表達式會比較簡單,只是一個單獨的名字,好比:
{{ user_name }}
有時候會至關複雜,包含一系列的屬性和過濾器(filters):
{{ user.name.localized|upper|escape }}
_expr_code
須要對上面各類狀況作出處理,實際複雜的表達式也是由簡單的表達式組合而成的,跟通常語言同樣,這裏用到了遞歸處理,完整的表達式經過|
分割,表達式內部還有點號.
分割。所以在函數定義的時候咱們採用可遞歸的形式:
def _expr_code(self, expr): """Generate a Python expression for 'expr'."""
函數內部首先考慮|
分割,若是有|
,就按照|
分割成多個表達式,而後對第一個元素進行遞歸處理:
if "|" in expr: pipes = expr.split('|') code = self._expr_code(pipes[0]) for func in pipes[1:]: self._variable(func, self.all_vars) code = "c_%s(%s)" % (func, code)
然後面的則是一系列的函數名。第一個表達式做爲參數傳遞到後面的這些函數中去,全部的函數也會被添加到all_vars
集合中以便例化
若是沒有|
,那麼可能有點號.
操做,那麼首先將開頭的表達式進行遞歸處理,後面再依次處理點好以後的表達式。
elif "." in expr: dots = expr.split('.') code = self._expr_code(dots[0]) args = ", ".join(repr(d) for d in dots[1:]) code = "do_dots(%s, %s)" % (code, args)
爲了理解點號是怎麼編譯的,咱們來回顧一下,在模板中x.y
可能表明x['y']
, x.y
甚至x.y()
。這種不肯定性意味着咱們須要在執行的過程當中依次對其進行嘗試,而不能再編譯時就去定義。所以咱們把這部分編譯爲一個函數調用do_dots(x, 'y', 'z')
,這個函數將會對各類情形進行遍歷並返回最終的結果值。
do_dots
函數已經傳遞到咱們編譯的結果函數中去了。它的實現稍後就會講到。
最後要處理的就是沒有|
和.
的部分,這種狀況下,這些就是簡單的變量名,咱們只須要將他們添加到all_vars
集合,而後同帶前綴的名字去獲取便可:
else: self._variable(expr, self.all_vars) code = "c_%s" % expr return code
剩下的工做就是編寫渲染代碼。既然咱們已經將模板編譯爲Python代碼,這裏工做量就大大減小了。這部分主要準備數據字典,並調用編譯的python代碼便可:
def render(self, context=None): """Render this template by applying it to `context`. `context` is a dictionary of values to use in this rendering. """ # Make the complete context we'll use. render_context = dict(self.context) if context: render_context.update(context) return self._render_function(render_context, self._do_dots)
記住,在咱們例化Templite
的時候就已經初始化了一個數據字典。這裏咱們將他複製,並將其與新的字典進行合併。拷貝的目的在於使各次的渲染數據獨立,而合併則能夠將字典簡化爲一個,有利於初始數據和新數據的統一。
另外,寫入到render的數據字典可能覆蓋例化Templite
時的初始值,但實際上例化時的字典有全局的一些東西,好比過濾器定義或者常量定義,而傳入到render
中的數據通常是特殊數據。
最後咱們只須要調用_render_function
方法,第一個參數是數據字典,第二個參數是_do_dots
的實現函數,是每次都相同的自定義函數,實現以下:
def _do_dots(self, value, *dots): """Evalute dotted expressions at runtime""" for dot in dots: try: value = getattr(value, dot) except AttributeError: value = value[dot] if callable(value): value = value() return value
在編譯過程當中,模板中像x.y.z
的代碼會被編譯爲「do_dots(x, ‘y’, ‘z’). 在函數中會對各個名字進行遍歷,每一次都會先嚐試獲取屬性值,若是失敗,在嘗試做爲字典值獲取。這樣使得模板語言更加靈活。在每次遍歷時還會檢測結果是否是能夠調用的函數,若是能夠調用就會對函數進行調用,並返回結果。
這裏,函數的參數列表定義爲(*dots)
,這樣就能夠得到任意數目的參數,這一樣使模板設計更爲靈活。
注意,在調用self._render_function
的時候,咱們傳進了一個函數,一個固定的函數。能夠認爲這個是模板編譯的一部分,咱們能夠直接將其編譯到模板,可是這樣每一個模板都須要一段相同的代碼。將這部分代碼提取出來會使得編譯結果更加簡單。
假設須要對整個代碼進行詳盡的測試以及邊緣測試,那麼代碼量可能超過500行,如今模板引擎只有252行代碼,測試代碼就有275行。測試代碼的數量多於正是代碼是個比較好的的測試代碼。
完整的代碼引擎將會實現更多的功能,爲了精簡代碼,咱們省略了以下的功能:
else
和elif
的複雜邏輯即使如此,咱們的模板引擎也十分有用。實際上這個引擎被用在coverage.py
中以生成HTML報告。
經過252行代碼,咱們實現了一個簡單的模板引擎,雖然實際引擎須要更多功能,可是這其中包含了不少基本思想:將模板編譯爲python代碼,而後執行代碼獲得最終結果。