关于简介和模板问题请在参考文档查看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代码,而后执行代码获得最终结果。