從零開始一個模板引擎的python實現(xiàn)——500 lines or less-A Template Engine翻譯(下)

500 lines or less 是一系列非常經(jīng)典而相對短小的python文章,每一章代碼不超過500行,卻實現(xiàn)了一些強大的功能梗夸,由業(yè)內(nèi)大牛執(zhí)筆借浊,有很大的學習價值塘淑。適合新手了解基本概念,也適合用來python進階蚂斤。
本篇原文
源碼
其他的一些開源的翻譯文章

翻譯上篇

寫引擎

既然我們已經(jīng)懂得了這個引擎要做什么存捺,讓我們來實現(xiàn)它。

The Templite class

模板引擎的核心是Templite類曙蒸。(它是一個模板捌治,但是它是精簡版的)
這個類具有一個小的接口。你可以利用模板中的文本構(gòu)建一個Templite對象纽窟,然后你可以使用它的render方法來渲染一個特定的上下文(數(shù)據(jù)的字典)到模板中。

# 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'],
})

我們將模板中的文本在對象創(chuàng)建時傳遞給它臂港,這樣我們就能只做一次編譯步驟视搏,然后多次調(diào)用render函數(shù)來重用編譯結(jié)果县袱。
構(gòu)造函數(shù)也接受一個字典來作為初始的上下文。這些數(shù)據(jù)被存儲在Templite對象里式散,并且之后當模板被渲染時可以獲取筋遭。這個位置適合于一些我們希望能隨時獲取的函數(shù)和常量,比如之前例子中的upper函數(shù)杂数。
在我們討論Templite的實現(xiàn)之前宛畦,我們需要先定義一個助手:CodeBuilder。

CodeBuilder

我們的模板引擎的主要工作是解析模板并產(chǎn)生必要的python代碼揍移。為了幫助產(chǎn)生python代碼次和,我們創(chuàng)建了一個CodeBuiler類,當我們構(gòu)建python代碼時它為我們處理簿記那伐。它增加代碼行踏施,管理縮進,最終給我們編譯好的python代碼罕邀。
一個CodeBuilder對象對一整塊python代碼負責畅形。對于我們的模板引擎,python塊始終是一個完整的函數(shù)定義诉探。但是CodeBuilder類并不假設它只是一個函數(shù)日熬,這讓CodeBuilder更通用,并且與余下的模板引擎代碼的耦合度低肾胯。
正如我們所看到的竖席,我們也使用嵌套的CodeBuilders 來讓把代碼放在函數(shù)的開始變得可能,即使我們可能直到完成才知道它到底做了什么敬肚。
一個CodeBuilder對象保存一個字符串列表毕荐,該列表將被組合到最終的python代碼。它唯一需要的其它狀態(tài)是當前的縮進級別:

class CodeBuilder(object):
    """Build source code conveniently."""

    def __init__(self, indent=0):
        self.code = []
        self.indent_level = indent

CodeBuilder并沒有做太多艳馒。add_line添加了一行新代碼憎亚,它會自動縮進到當前縮進級別,并提供一個換行符第美。

    def add_line(self, line):
        """Add a line of source to the code.

        Indentation and newline will be added for you, don't provide them.

        """
        self.code.extend([" " * self.indent_level, line, "\n"])

indentdedent提高和降低當前的縮進級別:

    INDENT_STEP = 4      # PEP8 says so!

    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列表主要是一列字符串庸毫,但是也保存了對CodeBuilder片段的引用飒赃。

    def add_section(self):
        """Add a section, a sub-CodeBuilder."""
        section = CodeBuilder(self.indent_level)
        self.code.append(section)
        return section

__str__產(chǎn)生單個字符串载佳,只是簡單地把self.code中的所有字符串組合在一起蔫慧。注意姑躲,因為self.code中包含其他CodeBuilder片段,這可能會遞歸調(diào)用其它的CodeBuilder對象的__str__方法屎开。

    def __str__(self):
        return "".join(str(c) for c in self.code)

get_globals產(chǎn)生執(zhí)行代碼的最終值侮繁。它字符串化對象,執(zhí)行它并得到它的定義娩贷,然后返回最終的值:

    def get_globals(self):
        """Execute the code, and return a dict of globals it defines."""
        # A check that the caller really finished all the blocks they started.
        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函數(shù)執(zhí)行一串包含python代碼的字符串甜熔,它的第二個參數(shù)是一個字典盆昙,用來收集字符串代碼中定義的全局變量淡喜。舉例來說炼团,如果我們這樣做:

python_source = """\
SEVENTEEN = 17

def three():
    return 3
"""
global_namespace = {}
exec(python_source, global_namespace)

然后global_namespace['SEVENTEEN']就是17,global_namespace['three']就是一個名為three的函數(shù)锌俱。
盡管我們只用CodeBuilder來生成一個函數(shù)嚼鹉,但是并沒有什么用來限制它锚赤。這使得這個類易于實現(xiàn)和理解线脚。
CodeBuilder讓我們創(chuàng)建python源代碼塊,而且一點也沒有關于我們的模板引擎的特定知識寓落。我們可以在python中定義三個不同的函數(shù)伶选,然后get_globals返回三個函數(shù)的字典仰税。這樣陨簇,我們的模板引擎只需要定義一個函數(shù)己单。但是更好的軟件設計方法是保留實現(xiàn)細節(jié)在模板引擎代碼中荷鼠,而不是在CodeBuilder類中允乐。
即使我們真正用它來定義單個函數(shù)牍疏,擁有一個返回字典的get_globals函數(shù)使代碼更加模塊化,因為它并不需要知道我們所定義的函數(shù)名稱厦滤。不論我們在python源中如何定義函數(shù)名掏导,我們都可以通過get_globals返回的字典來獲取它。
現(xiàn)在我們可以實現(xiàn)Templite類了梅屉,以及看看CodeBuilder是怎樣使用的虐唠。

Templite類的實現(xiàn)

我們的大部分代碼都在Templite類中凿滤。正如我們之前討論的眷蚓,它同時具有編譯階段和渲染階段叉钥。

編譯

編譯一個模板為python函數(shù)的所有工作在Templite構(gòu)造器里發(fā)生投队。首先上下文被保存:

    def __init__(self, text, *contexts):
        """Construct a Templite with the given `text`.

        `contexts` are dictionaries of values to use for future renderings.
        These are good for filters and global values.

        """
        self.context = {}
        for context in contexts:
            self.context.update(context)

注意我們使用了*contexts作為參數(shù)息楔。星號表示任意數(shù)量的位置參數(shù)將被打包成一個元組作為contexts傳遞進來。這叫做參數(shù)解包碟案,意味著調(diào)用者可以提供多個不同的上下文字典≡赶眨現(xiàn)在,如下調(diào)用都是有效的:

t = Templite(template_text)
t = Templite(template_text, context1)
t = Templite(template_text, context1, context2)

上下文參數(shù)在存在的情況下被作為一個元組提供給構(gòu)造器扮叨。我們可以遍歷這個元組,輪流處理它們每一個惦费。我們簡單的創(chuàng)建了一個所有上下文字典組合而成的字典,叫做self.context薪贫。如果有重復的字典值扯夭,后面的會覆蓋前面的。
為了使用編譯出來的函數(shù)運行得盡可能的快橡淑,我們將上下文中的變量提取到python本地變量中构拳。我們將通過保存一個遇到過的變量名的集合來獲取它們,但是我們也需要跟蹤模板中定義的變量名,如循環(huán)變量:

        self.all_vars = set()
        self.loop_vars = set()

稍后我們將看到這些是如何被用來幫助構(gòu)建函數(shù)的序幕的置森。首先斗埂,我們用了之前寫的CodeBuilder類來開始構(gòu)建我們的編譯函數(shù):

        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")

在這里,我們構(gòu)建了我們的CodeBuilder對象凫海,然后向里面加入語句呛凶。我們的python函數(shù)將被稱為render_function,它接受兩個參數(shù):一個是上下文數(shù)據(jù)字典,一個是實現(xiàn)點屬性訪問的do_dots函數(shù)。
這里的上下文是兩個上下文數(shù)據(jù)的組合:被傳遞給Templite構(gòu)造器的上下文和被傳給渲染函數(shù)的上下文。這是我們在Templite構(gòu)造器中創(chuàng)建的模板可獲得的一套完整的數(shù)據(jù)泪勒。
請注意夫植,CodeBuilder很簡單:它不知道函數(shù)的定義沈跨,只擁有代碼行。這保持CodeBuilder在實現(xiàn)和使用上的簡便性。這里碑隆,我們可以讀取我們生成的代碼而不需要在精神上插入太多的專門的CodeBuilder拴疤。
我們創(chuàng)建了一個片段叫vars_code。之后我們將在該片段中寫上變量提取的語句罚随。vars_code對象使我們保留了一個函數(shù)中的位置,它將在我們得到需要的信息后被填補微服。
隨后是添加四條固定語句魄缚,定義了一個結(jié)果列表,添加了列表方法和內(nèi)置str方法的快捷方式匿乃。正如我們之前討論的佛嬉,這個奇怪的步驟為我們的渲染函數(shù)擠出來一點點的性能提升苞氮。
同時擁有appendextend方法的快捷方式使我們面對一行或者多行的添加時,可以選擇最有效率的一個潜必。
接下來我們定義一個內(nèi)部函數(shù)來幫助我們緩沖輸出字符串:

        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[:]

當我們創(chuàng)建一堆需要加入編譯出的函數(shù)的輸出塊時磁滚,我們需要把它們變成加入result列表的函數(shù)調(diào)用。我們將反復的append調(diào)用組合為一個extend調(diào)用。這是另一個微優(yōu)化耕陷。要做到這一點猾警,我們緩沖這些輸出塊温自。
緩沖列表保存還未被寫入函數(shù)源代碼的字符串蔓钟。當我們的模板編譯運行時臭埋,我們將向buffered添加字符串,然后當我們遇到控制流節(jié)點(如if語句臀玄,循環(huán)的開始或末端)時瓢阴,將它們刷新到函數(shù)源代碼。
flus_output函數(shù)是一個閉包健无,閉包是對于一個引用本身之外變量的函數(shù)的花哨稱呼荣恐。在這里,flus_output引用了bufferedcode睬涧。這簡化了我們的函數(shù)調(diào)用:我們不必告訴flush_output刷新哪個緩沖區(qū)或者刷新到哪募胃,它隱式地知道這些。
如果只有只有一個字符串被緩沖畦浓,那么append_result將被調(diào)用痹束;如果多于一個,extend_result被使用讶请。然后緩沖隊列被清空來緩沖下一批的字符串祷嘶。
余下的編譯代碼將是添加語句到緩沖隊列。然后最終調(diào)用flush_output來將它們寫入CodeBuilder夺溢。
有了這個函數(shù)论巍,我們可以在編譯器中擁有這樣一條代碼:

buffered.append("'hello'")

這意味著我們編譯出來的python函數(shù)將有這樣一句:

append_result('hello')

hello字符串將被添加到模板的渲染輸出中。這里风响,我們有幾個層級的抽象嘉汰,很容易搞混。
編譯器使用了buffered.append("'hello'")來創(chuàng)建一個append_result('hello')語句在編譯出的python函數(shù)中状勤,而這個函數(shù)語句運行后添加了hello字符串到最終的模板結(jié)果中鞋怀。

回到我們的Templite類中双泪。當我們解析控制流結(jié)構(gòu)時,我們希望檢查它們是否是合理地嵌套了密似。ops_stack列表是一個字符串堆棧:

        ops_stack = []

例如當我們碰到一個{% if .. %}標簽焙矛,我們將'if'壓入堆棧。當我們碰到一個{% endif %}標簽時残腌,我們再將之前的'if'彈出堆棧村斟。如果棧頂沒有'if'則報告錯誤。

現(xiàn)在真正的解析開始抛猫。我們使用正則表達式將模板文本分割多個標志蟆盹。正則表達式可能是令人畏懼的:它們是非常緊湊的符號,用來做復雜的模式匹配闺金。它們也非常高效日缨,因為模式匹配的復雜部分是正則表達式引擎中用C實現(xiàn)的,而不是你自己的python代碼掖看。這是我們的正則表達式:

tokens = re.split(r"(?s)({{.*?}}|{%.*?%}|{#.*?#})", text)

這看上去很復雜,來讓我們分解它面哥。
re.split函數(shù)將使用正則分割一個字符串哎壳。我們的模式在括號里,匹配的符號將被用來分割字符串尚卫,分割出的字符串將組成列表返回归榕。我們的模式代表了我們的標簽語法,我們將它括起來使字符串會在標簽處被分割吱涉,然后標簽也會被返回刹泄。
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
]

一旦文本被分割成這樣的標記特石,我們就可以循環(huán)依次處理它們。根據(jù)類型來分割它們鳖链,我們就可以分別處理每個類型姆蘸。
編譯代碼是一個關于這些標記的循環(huán):

        for token in tokens:

每個標記都被檢查,看它是四種情況中的哪一個芙委。只看頭兩個字符就夠了逞敷。第一種情況是注釋,只需要忽略它然后繼續(xù)處理下一個標記就行了:

            if token.startswith('{#'):
                # Comment: ignore it and move on.
                continue

對于{{...}}表達式灌侣,我們截斷前后的大括號推捐,用空格分割,然后整個傳遞給_expr_code函數(shù):

            elif token.startswith('{{'):
                # An expression to evaluate.
                expr = self._expr_code(token[2:-2].strip())
                buffered.append("to_str(%s)" % expr)

_expr_code方法將編譯模板表達式為python語句侧啼。留后再看牛柒。我們使用了to_str函數(shù)來強制返回的表達式的值為字符串堪簿,然后將它加到我們的結(jié)果列表中糕韧。
第三種情況是{% ... %}標簽垃它。要將它們變?yōu)閜ython的控制結(jié)構(gòu)贿讹。首先我們刷新我們的輸出語句緩沖隊列貌笨,然后我們從標簽中提取單詞列表:

            elif token.startswith('{%'):
                # Action tag: split into words and parse further.
                flush_output()
                words = token[2:-2].strip().split()

現(xiàn)在我們有三個子情況匾鸥,取決于標簽的第一個詞:if,for或者end壳快。if情況至少展示了簡單的錯誤處理和代碼生成:

                if words[0] == 'if':
                    # An if statement: evaluate 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列表只應該有兩個元素粥喜。如果不是畏腕,我們利用_syntax_error輔助方法來拋出一個語法異常缴川。我們將'if'壓入ops_stack棧中,來讓我們檢查相應的endif標簽描馅。'if'標簽的表達式部分通過_expr_code編譯為python表達式把夸,然后被用作python中if語句的條件表達式。
第二個標簽的類型是for铭污,它將被編譯為一個python的for語句:

                elif words[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._variable(words[1], self.loop_vars)
                    code.add_line(
                        "for c_%s in %s:" % (
                            words[1],
                            self._expr_code(words[3])
                        )
                    )
                    code.indent()

我們做了一個語法檢查并且將for壓入棧中恋日。_variable方法檢查了變量的語法,并且將它加入我們提供的集合嘹狞。這就是我們在編譯過程中收集所有變量的名稱的方法岂膳。之后我們需要編寫我們的函數(shù)的序幕,那時我們將解包所有從上下文得到的變量名磅网。為了能正確地完成該操作谈截,我們需要知道所有我們碰到過的變量名,self.all_vars和所有循環(huán)中定義的變量名涧偷,self.loop_vars簸喂。
然后我們添加一行for語句到我們的函數(shù)源碼中。所有的模板變量都加上c_前綴被轉(zhuǎn)換為python變量燎潮,所以我們知道它們不會與其它命名沖突喻鳄。我們使用_expr_code函數(shù)來編譯模板中的迭代表達式到python中的迭代表達式。

最后一種標簽就是end了确封,不論是{% endif %}還是{% endfor %}诽表。效果對于我們編譯出的函數(shù)源碼是一樣的:只是簡單地在之前的iffor語句末尾加上取消縮進:

                elif words[0].startswith('end'):
                    # Endsomething.  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 ends", token)
                    start_what = ops_stack.pop()
                    if start_what != end_what:
                        self._syntax_error("Mismatched end tag", end_what)
                    code.dedent()

注意這里真正需要的工作只是最后一行:取消函數(shù)源碼的縮進。余下的語句都是錯誤檢查來保證模板被正確地組織了隅肥。這在程序翻譯代碼中很常見竿奏。
說道錯誤處理,如果標簽不是一個if腥放、for或者end泛啸,那么我們也不知道它是什么,所以拋出一個語法異常:

                else:
                    self._syntax_error("Don't understand tag", words[0])

我們對三個特殊語法({{...}}, {#...#}, 以及{%...%})的處理已經(jīng)完成了秃症。剩下的就是文字內(nèi)容候址。我們添加文字內(nèi)容到緩沖輸出隊列吕粹,記得使用內(nèi)置的repr函數(shù)來產(chǎn)生一個python字符串字面量:

            else:
                # Literal content.  If it isn't empty, output it.
                if token:
                    buffered.append(repr(token))

否則,可能會在我們編譯出的函數(shù)中出現(xiàn)下面的語句:

append_result(abc)      # Error! abc isn't defined

我們需要值被這樣引用:

append_result('abc')

repr函數(shù)提供了對字符串的引用岗仑,并且會在需要的地方提供反斜杠:

append_result('"Don\'t you like my hat?" he asked.')

注意我們一開始用if token:檢查了該標記是否是空的匹耕,因為添加一個空字符串到輸出中是沒有意義的。因為我們的正則表達式是按標簽語法分割的荠雕,鄰近的標簽會造成一個空標記在它倆之間稳其。這里的檢查是一種避免無用的append_result("")語句出現(xiàn)在編譯出的函數(shù)中的簡易方法。
這樣就完成了模板中所有標記的循環(huán)炸卑。當循環(huán)結(jié)束既鞠,模板中的所有地方都被處理了。我們還有一個檢查要做盖文,那就是如果ops_stack不為空嘱蛋,我們一定漏掉了一個結(jié)束標簽。然后我們刷新緩沖隊列輸出到函數(shù)源碼五续。

        if ops_stack:
            self._syntax_error("Unmatched action tag", ops_stack[-1])

        flush_output()

在函數(shù)的一開始我們已經(jīng)創(chuàng)建了一個片段洒敏。它的角色是從上下文中解包模板變量到python本地變量。既然我們已經(jīng)處理完了這個模板疙驾,我們也知道所有變量的名稱桐玻,我們就可以在函數(shù)序幕中寫語句。
我們必須做一點小工作來知道我們需要定義什么名稱荆萤。看我們的示例模板:

<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是在循環(huán)中定義的。
模板中所有的變量都在集合all_vars中蝉仇,模板中定義的變量都在loop_vars中旋讹。所有loop_vars中的變量名稱都已經(jīng)在代碼中被定義了,因為它們在循環(huán)中被使用了轿衔。所以我們需要解包任何屬于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))

每個名稱都變成函數(shù)序幕的一行代碼沉迹,將上下文變量解包成合法的本地變量。
我們快要完成將模板變?yōu)閜ython函數(shù)的編譯害驹。我們的函數(shù)一直在將字符串加入result中鞭呕,所以最后一行是簡單地將它們組合在一起并返回:

        code.add_line("return ''.join(result)")
        code.dedent()

既然我們已經(jīng)完成了編譯出的python函數(shù)的源碼的書寫,我們需要的就是從CodeBuilder對象得到函數(shù)本身宛官。get_globals方法執(zhí)行我們組裝好的python代碼葫松。記住我們的代碼是一個函數(shù)定義(以def render_function(..):開始)瓦糕,所以執(zhí)行這個代碼會定義render_function,但是并不執(zhí)行render_function的主體腋么。
get_globals的結(jié)果是代碼中定義的值的字典咕娄。我們從中獲取render_function的值,然后將它保存為Templite對象的一個屬性:

        self._render_function = code.get_globals()['render_function']

現(xiàn)在self._render_function就是一個可調(diào)用的python函數(shù)了珊擂。我們會在渲染階段使用它圣勒。

編譯表達式

我們還沒有看到編譯過程的一個重要部分:_expr_code方法,它將模板表達式編譯為python表達式未玻。我們的模板表達式可能簡單的只是一個名字:

{{user_name}}

也可能是一個復雜的序列包含屬性訪問和過濾器:

{{user.name.localized|upper|escape}}

我們的_expr_code方法將處理所有的可能情況灾而。正如所有語言中的表達式,我們的也是遞歸構(gòu)建的:大的表達式由小的表達式組成扳剿。一個完整的表達式由管道符分隔旁趟,其中第一部分是由逗號分隔的,諸如此類庇绽。所以我們的函數(shù)自然地采取遞歸的形式:

    def _expr_code(self, expr):
        """Generate a Python expression for `expr`."""

第一種情況是考慮我們的表達式中有管道分隔符锡搜。如果有,我們要分割它為一個管道片段列表瞧掺。第一部分將被遞歸地傳入_expr_code來將它轉(zhuǎn)換為一個python表達式耕餐。

        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)

余下的每一個管道片段都是一個函數(shù)名。值被傳遞給這些函數(shù)來產(chǎn)生最終的值辟狈。每一個函數(shù)名都是一個變量肠缔,要加入到all_vars中所以我們能在序幕中正確地提取它。
如果沒有管道哼转,可能會有點操作符明未。如果有的話,按點分割壹蔓。將第一部分遞歸地傳遞給_expr_code來將它轉(zhuǎn)換為一個python表達式趟妥,之后以點分割的名稱都被依次處理。

        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披摄,誰能工作用誰,如果結(jié)果是可調(diào)用的勇凭,就調(diào)用它疚膊。這種不確定性意味著我們不得不在運行時嘗試所有的可能,而不是在編譯時確定虾标。所以我們編譯x.y.z為一個函數(shù)調(diào)用酿联,do_dots(x, 'y', 'z')。點函數(shù)將嘗試不同的訪問方法并返回成功的值。
do_dots函數(shù)將在運行時傳入我們編譯好的python函數(shù)贞让。我們將看到它的實現(xiàn)周崭。
_expr_code函數(shù)最后的語句是處理沒有管道和點操作符的情況。這時喳张,表達式僅僅是一個名稱续镇。我們將它記錄在all_vars中,并且通過帶前綴的python命名獲取它:

        else:
            self._variable(expr, self.all_vars)
            code = "c_%s" % expr
        return code
輔助函數(shù)

在編譯過程中销部,我們使用了一些輔助函數(shù)摸航。比如_syntax_error方法僅僅組合出漂亮的錯誤信息并拋出異常:

    def _syntax_error(self, msg, thing):
        """Raise a syntax error using `msg`, and showing `thing`."""
        raise TempliteSyntaxError("%s: %r" % (msg, thing))

_variable方法幫助我們驗證變量名是否有效,然后將它們加入在編譯過程中我們收集的姓名集合舅桩。我么是用正則來檢查名稱是否是一個有效的python標識符酱虎,然后將它加入集合:

    def _variable(self, name, vars_set):
        """Track that `name` is used as a variable.

        Adds the name to `vars_set`, a set of variable names.

        Raises an syntax error if `name` is not a valid name.

        """
        if not re.match(r"[_a-zA-Z][_a-zA-Z0-9]*$", name):
            self._syntax_error("Not a valid name", name)
        vars_set.add(name)

就這樣,編譯部分的代碼都完成了擂涛。

渲染

剩下的就是編寫渲染代碼了读串。因為我們將模板編譯為python函數(shù),所以渲染部分沒有太多工作要做撒妈。它準備好上下文恢暖,然后調(diào)用編譯好的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)

想起來,我們在構(gòu)建Templite對象時狰右,我們從一個數(shù)據(jù)上下文開始杰捂。這里我們復制它,然后將它和渲染函數(shù)被傳入的數(shù)據(jù)混合棋蚌。復制是為了讓連續(xù)的多個渲染函數(shù)調(diào)用不會看到相互的數(shù)據(jù)嫁佳,然后將它們混合是為了讓我們只有一個字典來進行數(shù)據(jù)查找。這就是我們?nèi)绾螐奶峁┑亩鄠€上下文(模板被創(chuàng)建時和渲染時)中構(gòu)建出一個統(tǒng)一的數(shù)據(jù)上下文谷暮。

要注意的是蒿往,我們傳遞給render的數(shù)據(jù)可能會覆蓋傳遞給Templite構(gòu)造器的。這往往不會發(fā)生坷备,因為傳遞給構(gòu)造器的上下文包含的是全局定義的過濾器和常量,而傳給render的上下文包含的是那一次渲染的特有數(shù)據(jù)情臭。
然后我們簡要地調(diào)用我們的render_function省撑。第一個參數(shù)是完整的上下文數(shù)據(jù),第二個是將被實現(xiàn)的點語義函數(shù)俯在。我們每次使用同樣的實現(xiàn):我們的_do_dots方法竟秫。

    def _do_dots(self, value, *dots):
        """Evaluate 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被轉(zhuǎn)換為do_dots(x, 'y', 'z')跷乐。這個函數(shù)循環(huán)每個點后的名稱肥败,對每一個它先嘗試是否是一個屬性,不是的話再看它是否是一個字典的鍵。這給予了我們的單個模板語法一定的自由度來同時表示x.yx['y']馒稍。在每一步皿哨,我們都嘗試調(diào)用它。一旦我們完成了循環(huán)纽谒,value的值就是我們想要的值证膨。

這里我們再次使用了python的參數(shù)解包(*dots)以至于_do_dots能夠處理任意多的點操作。這增加了我們的函數(shù)的適用性鼓黔,能為所有模板中的點表達式工作央勒。

注意的是當調(diào)用self._render_function時,我們傳遞了一個函數(shù)來評定點表達式澳化,但是我們總是傳遞同一個崔步。我們能夠使它成為被編譯的模板的一部分,但是這些行是關于模板的工作方式缎谷,而不是特定模板的部分細節(jié)井濒。所以像這樣分開實現(xiàn)讓人感覺結(jié)構(gòu)更清晰。

測試

和模板引擎一起提供的是一系列測試覆蓋了所有行為和邊緣情況慎陵。我實際上有點超出500行的限制:模板引擎有252行眼虱,測試有275行。這是一個典型的測試完備的代碼:你的測試代碼比你的產(chǎn)品代碼還多席纽。

可以完善的地方

完備特性的模板引擎比我們在這里實現(xiàn)的要復雜得多捏悬。為了保證代碼量小,我們遺留了許多有趣的問題:

  • 模板繼承和包含
  • 自定義標簽
  • 自動換碼
  • 參數(shù)過濾器
  • 復雜條件邏輯如else和elif
  • 不止一個循環(huán)變量的循環(huán)
  • 空白的控制
    即便如此润梯,我們的簡單模板引擎也足夠有用了过牙。實際上,它被用來為coverage.py生成它的HTML報告纺铭。

總結(jié)

在252行中寇钉,我們得到了一個簡單但是有一定功能的模板引擎。真實的模板引擎具有更多的特性舶赔,但是這個代碼勾畫出整個過程的基本思路:將模板編譯成一個python函數(shù)扫倡,然后執(zhí)行這個函數(shù)來生成最終的文本結(jié)果。

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末竟纳,一起剝皮案震驚了整個濱河市撵溃,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌锥累,老刑警劉巖缘挑,帶你破解...
    沈念sama閱讀 212,816評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異桶略,居然都是意外死亡语淘,警方通過查閱死者的電腦和手機诲宇,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,729評論 3 385
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來惶翻,“玉大人姑蓝,你說我怎么就攤上這事∥兀” “怎么了它掂?”我有些...
    開封第一講書人閱讀 158,300評論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長溯泣。 經(jīng)常有香客問我虐秋,道長,這世上最難降的妖魔是什么垃沦? 我笑而不...
    開封第一講書人閱讀 56,780評論 1 285
  • 正文 為了忘掉前任客给,我火速辦了婚禮,結(jié)果婚禮上肢簿,老公的妹妹穿的比我還像新娘靶剑。我一直安慰自己,他們只是感情好池充,可當我...
    茶點故事閱讀 65,890評論 6 385
  • 文/花漫 我一把揭開白布桩引。 她就那樣靜靜地躺著,像睡著了一般收夸。 火紅的嫁衣襯著肌膚如雪坑匠。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 50,084評論 1 291
  • 那天卧惜,我揣著相機與錄音厘灼,去河邊找鬼。 笑死咽瓷,一個胖子當著我的面吹牛设凹,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播茅姜,決...
    沈念sama閱讀 39,151評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼闪朱,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了钻洒?” 一聲冷哼從身側(cè)響起奋姿,我...
    開封第一講書人閱讀 37,912評論 0 268
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎航唆,沒想到半個月后胀蛮,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體院刁,經(jīng)...
    沈念sama閱讀 44,355評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡糯钙,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,666評論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片任岸。...
    茶點故事閱讀 38,809評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡再榄,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出享潜,到底是詐尸還是另有隱情困鸥,我是刑警寧澤,帶...
    沈念sama閱讀 34,504評論 4 334
  • 正文 年R本政府宣布剑按,位于F島的核電站疾就,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏艺蝴。R本人自食惡果不足惜猬腰,卻給世界環(huán)境...
    茶點故事閱讀 40,150評論 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望猜敢。 院中可真熱鬧姑荷,春花似錦、人聲如沸缩擂。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,882評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽胯盯。三九已至懈费,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間陨闹,已是汗流浹背楞捂。 一陣腳步聲響...
    開封第一講書人閱讀 32,121評論 1 267
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留趋厉,地道東北人寨闹。 一個月前我還...
    沈念sama閱讀 46,628評論 2 362
  • 正文 我出身青樓,卻偏偏與公主長得像君账,于是被迫代替她去往敵國和親繁堡。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 43,724評論 2 351

推薦閱讀更多精彩內(nèi)容