从零开始一个模板引擎的python实现——500 lines or less-A Template Engine翻译(下)

500 lines or less 是一系列非常经典而相对短小的python文章,每一章代码不超过500行,却实现了一些强大的功能,由业内大牛执笔,有很大的学习价值。适合新手了解基本概念,也适合用来python进阶。
本篇原文
源码
其他的一些开源的翻译文章

翻译上篇

写引擎

既然我们已经懂得了这个引擎要做什么,让我们来实现它。

The Templite class

模板引擎的核心是Templite类。(它是一个模板,但是它是精简版的)
这个类具有一个小的接口。你可以利用模板中的文本构建一个Templite对象,然后你可以使用它的render方法来渲染一个特定的上下文(数据的字典)到模板中。

# 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函数来重用编译结果。
构造函数也接受一个字典来作为初始的上下文。这些数据被存储在Templite对象里,并且之后当模板被渲染时可以获取。这个位置适合于一些我们希望能随时获取的函数和常量,比如之前例子中的upper函数。
在我们讨论Templite的实现之前,我们需要先定义一个助手:CodeBuilder。

CodeBuilder

我们的模板引擎的主要工作是解析模板并产生必要的python代码。为了帮助产生python代码,我们创建了一个CodeBuiler类,当我们构建python代码时它为我们处理簿记。它增加代码行,管理缩进,最终给我们编译好的python代码。
一个CodeBuilder对象对一整块python代码负责。对于我们的模板引擎,python块始终是一个完整的函数定义。但是CodeBuilder类并不假设它只是一个函数,这让CodeBuilder更通用,并且与余下的模板引擎代码的耦合度低。
正如我们所看到的,我们也使用嵌套的CodeBuilders 来让把代码放在函数的开始变得可能,即使我们可能直到完成才知道它到底做了什么。
一个CodeBuilder对象保存一个字符串列表,该列表将被组合到最终的python代码。它唯一需要的其它状态是当前的缩进级别:

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__产生单个字符串,只是简单地把self.code中的所有字符串组合在一起。注意,因为self.code中包含其他CodeBuilder片段,这可能会递归调用其它的CodeBuilder对象的__str__方法。

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

get_globals产生执行代码的最终值。它字符串化对象,执行它并得到它的定义,然后返回最终的值:

    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函数执行一串包含python代码的字符串,它的第二个参数是一个字典,用来收集字符串代码中定义的全局变量。举例来说,如果我们这样做:

python_source = """\
SEVENTEEN = 17

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

然后global_namespace['SEVENTEEN']就是17,global_namespace['three']就是一个名为three的函数。
尽管我们只用CodeBuilder来生成一个函数,但是并没有什么用来限制它。这使得这个类易于实现和理解。
CodeBuilder让我们创建python源代码块,而且一点也没有关于我们的模板引擎的特定知识。我们可以在python中定义三个不同的函数,然后get_globals返回三个函数的字典。这样,我们的模板引擎只需要定义一个函数。但是更好的软件设计方法是保留实现细节在模板引擎代码中,而不是在CodeBuilder类中。
即使我们真正用它来定义单个函数,拥有一个返回字典的get_globals函数使代码更加模块化,因为它并不需要知道我们所定义的函数名称。不论我们在python源中如何定义函数名,我们都可以通过get_globals返回的字典来获取它。
现在我们可以实现Templite类了,以及看看CodeBuilder是怎样使用的。

Templite类的实现

我们的大部分代码都在Templite类中。正如我们之前讨论的,它同时具有编译阶段和渲染阶段。

编译

编译一个模板为python函数的所有工作在Templite构造器里发生。首先上下文被保存:

    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作为参数。星号表示任意数量的位置参数将被打包成一个元组作为contexts传递进来。这叫做参数解包,意味着调用者可以提供多个不同的上下文字典。现在,如下调用都是有效的:

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

上下文参数在存在的情况下被作为一个元组提供给构造器。我们可以遍历这个元组,轮流处理它们每一个。我们简单的创建了一个所有上下文字典组合而成的字典,叫做self.context。如果有重复的字典值,后面的会覆盖前面的。
为了使用编译出来的函数运行得尽可能的快,我们将上下文中的变量提取到python本地变量中。我们将通过保存一个遇到过的变量名的集合来获取它们,但是我们也需要跟踪模板中定义的变量名,如循环变量:

        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对象,然后向里面加入语句。我们的python函数将被称为render_function,它接受两个参数:一个是上下文数据字典,一个是实现点属性访问的do_dots函数。
这里的上下文是两个上下文数据的组合:被传递给Templite构造器的上下文和被传给渲染函数的上下文。这是我们在Templite构造器中创建的模板可获得的一套完整的数据。
请注意,CodeBuilder很简单:它不知道函数的定义,只拥有代码行。这保持CodeBuilder在实现和使用上的简便性。这里,我们可以读取我们生成的代码而不需要在精神上插入太多的专门的CodeBuilder。
我们创建了一个片段叫vars_code。之后我们将在该片段中写上变量提取的语句。vars_code对象使我们保留了一个函数中的位置,它将在我们得到需要的信息后被填补。
随后是添加四条固定语句,定义了一个结果列表,添加了列表方法和内置str方法的快捷方式。正如我们之前讨论的,这个奇怪的步骤为我们的渲染函数挤出来一点点的性能提升。
同时拥有appendextend方法的快捷方式使我们面对一行或者多行的添加时,可以选择最有效率的一个。
接下来我们定义一个内部函数来帮助我们缓冲输出字符串:

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

当我们创建一堆需要加入编译出的函数的输出块时,我们需要把它们变成加入result列表的函数调用。我们将反复的append调用组合为一个extend调用。这是另一个微优化。要做到这一点,我们缓冲这些输出块。
缓冲列表保存还未被写入函数源代码的字符串。当我们的模板编译运行时,我们将向buffered添加字符串,然后当我们遇到控制流节点(如if语句,循环的开始或末端)时,将它们刷新到函数源代码。
flus_output函数是一个闭包,闭包是对于一个引用本身之外变量的函数的花哨称呼。在这里,flus_output引用了bufferedcode。这简化了我们的函数调用:我们不必告诉flush_output刷新哪个缓冲区或者刷新到哪,它隐式地知道这些。
如果只有只有一个字符串被缓冲,那么append_result将被调用;如果多于一个,extend_result被使用。然后缓冲队列被清空来缓冲下一批的字符串。
余下的编译代码将是添加语句到缓冲队列。然后最终调用flush_output来将它们写入CodeBuilder。
有了这个函数,我们可以在编译器中拥有这样一条代码:

buffered.append("'hello'")

这意味着我们编译出来的python函数将有这样一句:

append_result('hello')

hello字符串将被添加到模板的渲染输出中。这里,我们有几个层级的抽象,很容易搞混。
编译器使用了buffered.append("'hello'")来创建一个append_result('hello')语句在编译出的python函数中,而这个函数语句运行后添加了hello字符串到最终的模板结果中。

回到我们的Templite类中。当我们解析控制流结构时,我们希望检查它们是否是合理地嵌套了。ops_stack列表是一个字符串堆栈:

        ops_stack = []

例如当我们碰到一个{% if .. %}标签,我们将'if'压入堆栈。当我们碰到一个{% endif %}标签时,我们再将之前的'if'弹出堆栈。如果栈顶没有'if'则报告错误。

现在真正的解析开始。我们使用正则表达式将模板文本分割多个标志。正则表达式可能是令人畏惧的:它们是非常紧凑的符号,用来做复杂的模式匹配。它们也非常高效,因为模式匹配的复杂部分是正则表达式引擎中用C实现的,而不是你自己的python代码。这是我们的正则表达式:

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

这看上去很复杂,来让我们分解它。
re.split函数将使用正则分割一个字符串。我们的模式在括号里,匹配的符号将被用来分割字符串,分割出的字符串将组成列表返回。我们的模式代表了我们的标签语法,我们将它括起来使字符串会在标签处被分割,然后标签也会被返回。
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.startswith('{#'):
                # Comment: ignore it and move on.
                continue

对于{{...}}表达式,我们截断前后的大括号,用空格分割,然后整个传递给_expr_code函数:

            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函数来强制返回的表达式的值为字符串,然后将它加到我们的结果列表中。
第三种情况是{% ... %}标签。要将它们变为python的控制结构。首先我们刷新我们的输出语句缓冲队列,然后我们从标签中提取单词列表:

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

现在我们有三个子情况,取决于标签的第一个词:if,for或者endif情况至少展示了简单的错误处理和代码生成:

                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方法检查了变量的语法,并且将它加入我们提供的集合。这就是我们在编译过程中收集所有变量的名称的方法。之后我们需要编写我们的函数的序幕,那时我们将解包所有从上下文得到的变量名。为了能正确地完成该操作,我们需要知道所有我们碰到过的变量名,self.all_vars和所有循环中定义的变量名,self.loop_vars
然后我们添加一行for语句到我们的函数源码中。所有的模板变量都加上c_前缀被转换为python变量,所以我们知道它们不会与其它命名冲突。我们使用_expr_code函数来编译模板中的迭代表达式到python中的迭代表达式。

最后一种标签就是end了,不论是{% endif %}还是{% endfor %}。效果对于我们编译出的函数源码是一样的:只是简单地在之前的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()

注意这里真正需要的工作只是最后一行:取消函数源码的缩进。余下的语句都是错误检查来保证模板被正确地组织了。这在程序翻译代码中很常见。
说道错误处理,如果标签不是一个iffor或者end,那么我们也不知道它是什么,所以抛出一个语法异常:

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

我们对三个特殊语法({{...}}, {#...#}, 以及{%...%})的处理已经完成了。剩下的就是文字内容。我们添加文字内容到缓冲输出队列,记得使用内置的repr函数来产生一个python字符串字面量:

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

否则,可能会在我们编译出的函数中出现下面的语句:

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

我们需要值被这样引用:

append_result('abc')

repr函数提供了对字符串的引用,并且会在需要的地方提供反斜杠:

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

注意我们一开始用if token:检查了该标记是否是空的,因为添加一个空字符串到输出中是没有意义的。因为我们的正则表达式是按标签语法分割的,邻近的标签会造成一个空标记在它俩之间。这里的检查是一种避免无用的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_nameproductall_vars集合将拥有他们的名称,因为它们都被用在{{...}}表达式中。但是只有user_name需要在序幕中从上下文提取,因为product是在循环中定义的。
模板中所有的变量都在集合all_vars中,模板中定义的变量都在loop_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))

每个名称都变成函数序幕的一行代码,将上下文变量解包成合法的本地变量。
我们快要完成将模板变为python函数的编译。我们的函数一直在将字符串加入result中,所以最后一行是简单地将它们组合在一起并返回:

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

既然我们已经完成了编译出的python函数的源码的书写,我们需要的就是从CodeBuilder对象得到函数本身。get_globals方法执行我们组装好的python代码。记住我们的代码是一个函数定义(以def render_function(..):开始),所以执行这个代码会定义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}}

也可能是一个复杂的序列包含属性访问和过滤器:

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

我们的_expr_code方法将处理所有的可能情况。正如所有语言中的表达式,我们的也是递归构建的:大的表达式由小的表达式组成。一个完整的表达式由管道符分隔,其中第一部分是由逗号分隔的,诸如此类。所以我们的函数自然地采取递归的形式:

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

第一种情况是考虑我们的表达式中有管道分隔符。如果有,我们要分割它为一个管道片段列表。第一部分将被递归地传入_expr_code来将它转换为一个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)

余下的每一个管道片段都是一个函数名。值被传递给这些函数来产生最终的值。每一个函数名都是一个变量,要加入到all_vars中所以我们能在序幕中正确地提取它。
如果没有管道,可能会有点操作符。如果有的话,按点分割。将第一部分递归地传递给_expr_code来将它转换为一个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,谁能工作用谁,如果结果是可调用的,就调用它。这种不确定性意味着我们不得不在运行时尝试所有的可能,而不是在编译时确定。所以我们编译x.y.z为一个函数调用,do_dots(x, 'y', 'z')。点函数将尝试不同的访问方法并返回成功的值。
do_dots函数将在运行时传入我们编译好的python函数。我们将看到它的实现。
_expr_code函数最后的语句是处理没有管道和点操作符的情况。这时,表达式仅仅是一个名称。我们将它记录在all_vars中,并且通过带前缀的python命名获取它:

        else:
            self._variable(expr, self.all_vars)
            code = "c_%s" % expr
        return code
辅助函数

在编译过程中,我们使用了一些辅助函数。比如_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函数,所以渲染部分没有太多工作要做。它准备好上下文,然后调用编译好的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):
        """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被转换为do_dots(x, 'y', 'z')。这个函数循环每个点后的名称,对每一个它先尝试是否是一个属性,不是的话再看它是否是一个字典的键。这给予了我们的单个模板语法一定的自由度来同时表示x.yx['y']。在每一步,我们都尝试调用它。一旦我们完成了循环,value的值就是我们想要的值。

这里我们再次使用了python的参数解包(*dots)以至于_do_dots能够处理任意多的点操作。这增加了我们的函数的适用性,能为所有模板中的点表达式工作。

注意的是当调用self._render_function时,我们传递了一个函数来评定点表达式,但是我们总是传递同一个。我们能够使它成为被编译的模板的一部分,但是这些行是关于模板的工作方式,而不是特定模板的部分细节。所以像这样分开实现让人感觉结构更清晰。

测试

和模板引擎一起提供的是一系列测试覆盖了所有行为和边缘情况。我实际上有点超出500行的限制:模板引擎有252行,测试有275行。这是一个典型的测试完备的代码:你的测试代码比你的产品代码还多。

可以完善的地方

完备特性的模板引擎比我们在这里实现的要复杂得多。为了保证代码量小,我们遗留了许多有趣的问题:

  • 模板继承和包含
  • 自定义标签
  • 自动换码
  • 参数过滤器
  • 复杂条件逻辑如else和elif
  • 不止一个循环变量的循环
  • 空白的控制
    即便如此,我们的简单模板引擎也足够有用了。实际上,它被用来为coverage.py生成它的HTML报告。

总结

在252行中,我们得到了一个简单但是有一定功能的模板引擎。真实的模板引擎具有更多的特性,但是这个代码勾画出整个过程的基本思路:将模板编译成一个python函数,然后执行这个函数来生成最终的文本结果。

推荐阅读更多精彩内容