美文网首页Python 拓展python我爱编程
从零开始一个模板引擎的python实现——500 lines o

从零开始一个模板引擎的python实现——500 lines o

作者: treelake | 来源:发表于2016-09-19 21:08 被阅读3022次

    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函数,然后执行这个函数来生成最终的文本结果。

    相关文章

      网友评论

      • StanleyXX:机器翻译还是很难还原原文意思。

      本文标题:从零开始一个模板引擎的python实现——500 lines o

      本文链接:https://www.haomeiwen.com/subject/pxngettx.html