python 模板實現-引擎的編寫(有時間試一下)

關於簡介和模板問題請在參考文檔查看python

參考文檔:

1.模板的編寫:https://blog.csdn.net/MageeLen/article/details/68920913正則表達式

 

1、引擎的編寫

1.Templite 類

模板引擎的核心就是這個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。工具

2.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方法將添加一個新的代碼行,縮進將自動添加

  •  

indentdedent增長和減小縮進級別的函數:

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類,看一下如何去實現這樣一個類

2、Templite類的實現

就像以前咱們所講的同樣,咱們的主要任務在於實現模板發解析和渲染。

編譯(解析Compiling)

這部分工做須要完成模板代碼到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函數是一個閉包,裏面的變量包括bufferedcode。這樣咱們之後調用的時候就不須要指定寫入那個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()

目前支持的語法標籤主要包含三種結構:ifforend. 咱們來看看對於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()

注意,這裏結束標籤最重要的功能就是結束函數代碼塊,減小縮進。其餘的都是一些語法檢查,這種操做在翻譯模式通常都是沒有的。

說到錯誤處理,若是標籤不是iffor或者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_nameproduct。這些變量在模板遍歷後都會放到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

3、渲染

剩下的工做就是編寫渲染代碼。既然咱們已經將模板編譯爲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行。測試代碼的數量多於正是代碼是個比較好的的測試代碼。

未涉及的地方

完整的代碼引擎將會實現更多的功能,爲了精簡代碼,咱們省略了以下的功能:

  • 模板繼承和包含
  • 自定義標籤
  • 自動轉義
  • 參數過濾器
  • 例如elseelif的複雜邏輯
  • 多於一個變量的循環
  • 空白符控制

即使如此,咱們的模板引擎也十分有用。實際上這個引擎被用在coverage.py中以生成HTML報告。

總結

經過252行代碼,咱們實現了一個簡單的模板引擎,雖然實際引擎須要更多功能,可是這其中包含了不少基本思想:將模板編譯爲python代碼,而後執行代碼獲得最終結果。

相關文章
相關標籤/搜索