老师问小明:已经a=1,
求a+1的值。
小明挠挠头,思考后回答:老师,a+1的结果是2。
以上是一个非常简单的例子,实际上就是一个模板编译过程。
a=1,表示一个命名空间(namespace)中的一项,该命名空间中存在一个名称(name)为a,其值为1。在python里面,命名空间可以用数据结构字典来表示。
a+1,实际上是一个非常简单的模板。这个模板里面有且只有一条规则a+1。本博文中所说的模板,实际上更类似一份空白的数学考卷。这份数学考卷中,有很多类似上面老师问小明的题目。
学生解决这份考卷的过程,就是一个模板的编译过程。学生解答每一道题目获得答案,类似于模板每一个节点编译。一个模板是由很多节点组成。这样看来我们解决“模板编译”问题,可以转化成解决各个节点编译问题。反过来说,只要各个节点的编译问题解决了,整个模板的编译也就解决了。我们通常将一个大问题,转化成各个并行不相干的小问题(可以认为:一个大问题等于n个小问题相加),然后各个击破。
不过有时候,会遇到比a+1更加难一点的问题:
老师对小明说:现在要求a+1的值,但现在a是一个未知数,只知道a=b+1,
不过我们知道b=1。
小明思考了一分钟,对老师说:a+1等于3,
我们知道要想解决a+1的值,a的值首先要获得,我们称解决a的值是解决a+1问题不可或缺的一个嵌套子问题。只有这个嵌套子问题获得解决,原来的问题才能获得解决的。所幸,老师提供a=b+1,而且知道了b=1,那么小明可以知道a=2,进而知道a+1等于3。这样思考过程,本质上对应于递归算法解决问题的思路。那么我们可以用计算机模拟这种问题的编译。
总结一下数学考试的过程,答一份数学考卷的过程。如果答一份数学考卷交给机器人来做,会用到怎样的算法实现?
伪代码如下:
def solved_big_problem(big_problem):
small_problem_list = split(big_problem)
answer = ""
foreach small_problem in small_problem_list:
if is_problem_small_enough(small_problem):
answer+=small_problem.done()
else:
answer+=solved_big_problem(big_problem)
return answer
tornado template引擎中,将整个静态模板划分成不同类型的节点。
在我认为,template中主要有三种类型的节点(Node)
1. 文本节点(TextNode)
2. 注释节点(CommentNode)
3. 表达式节点(ExpressionNode)
4. 块节点(BlockNode)
一个文本节点是一个不会被命名空间数据渲染的字符串,也就是模板中,该字符串是什么,最后编译结束后输出的就是什么。
一个注释节点,以特殊标签”{#”开始,以“#}”结束,中间是一个对模板的注释字符串,注释节点在编译结束后,不会产生任何输出。
一个表达式节点,以特殊标签”((“开始,以“}}”结束,中间是一个字符串,这个字符串可能仅仅是一个名称的名字,编译后会被命名空间中该名称对应的值替换掉。也可能是一个字面量。更或者是一个复杂的表达式,在c++语言中,我们说一个表达式可以作为右值,用在等号的右边,赋值给一个变量。表达式的编译结果会被给表达式实际的值所替换。一个表达式中可能包含运算或者函数调用。
块节点是节点中最复杂的一种。通常以”{%”开始,以“ %}”结束。“{%…%}”中间的第一个单词,称之为指令(operator)。多个块节点将组合成更大的模板单位。我们将看到这些模板单位,会被映射成python语言中的,控制块, 循环体, 异常处理块,语句等等。
指令 | 用法 |
---|---|
apply | {% apply function %}…{% end %} |
autoescape | {% autoescape function %} |
block | {% block name %}…{% end %} |
comment | {% comment … %} |
extends | {% extends filename %} |
for | {% for var in expr %}…{% end %} |
from | {% from x import y %} |
if | {% if condition %}…{% elif condition %}…{% else %}…{% end %} |
import | {% import module %} |
include | {% include filename %} |
module | {% module expr %} |
raw | {% raw expr %} |
set | {% set x = y %} |
try | {% try %}…{% except %}…{% else %}…{% finally %}…{% end %} |
while | {% while condition %}… {% end %} |
break | {% break %} |
continue | {% continue %} |
模板解析的过程实际上就是找出静态模板字符串中各种节点。
class _Node(object):
def each_child(self):
return ()
def generate(self, writer):
raise NotImplementedError()
def find_named_blocks(self, loader, named_blocks):
for child in self.each_child():
child.find_named_blocks(loader, named_blocks)
def each_child(self):
一个节点的话,可能会包含很多子节点,提供这个接口,返回该节点所有子节点。
def generate(self, writer):
将该节点映射成python语言代码
def find_named_blocks(self, loader, named_blocks):
找到节点body中包含的命名块,此接口在处理模板继承时候作用重大。
_Node子类 | 表示节点类型 |
---|---|
_File | 整个模板 |
_ChunkList | 多个并列节点的集合,也可作为一个节点的body |
_NamedBlock | 命名块,对应block指令 ,在模板继承中起作用 |
_ExtendsBlock | 对应extends指令 |
_IncludeBlock | 对应include指令 |
_ApplyBlock | 对应apply指令 |
_ControlBlock | 对应if指令,控制块 |
_IntermediateControlBlock | 对应else,elif,except,finally指令 |
_Statement | 对应import,from,set,break,continue指令 |
_Expression | 对应raw指令 |
_Module | 对应module指令 |
_Text | 对应文本节点 |
tornado模板节点按是否含有子节点来说,其实只分为两类。和”{% end %}”结合才起作用的节点,称之为宿主节点。不必和”{% end %}”结合就能起作用的节点,称之为”直接节点“。另外称”{% end %}”为结束节点。
宿主节点不是一个简单的节点,里面可以寄居若干子节点,形成树状层级结构。直接节点可能是一个表达式,或者语句,不含有子节点,这种情况是很容易处理的。
而“宿主块节点”该如何解析?其实类似于我们熟悉的问题-括号匹配。因为一个宿主块节点和“{% end %}”,结合才起作用,类似一对括号,只有左右括号都存在的时候,才是一个完整的括号,起到它应该有的意义。
一般上过数据结构课的人都知道,可以通过数据结构栈,解决括号匹配问题,其实也可以解决宿主节点解析问题。
如果宿主节点含有子节点,如下模板代码。
{% block content %}
{% for line in lines %}
<p>line</p>
{% end %}
{% if user=="admin" %}
<p>welcome admin!</p>
{% end %}
<p>just test</p>
{% end %}
我们换另外一种思路,递归思想来考虑这个问题,发现也非常直接。简单伪代码如下(流程不太严谨)。
def parse(template_string):
child_nodes = []
while !template_string.end():
node = get_next_node(template_string)
if is_end_node(node):
return child_nodes
if is_has_child_nodes(node):
template_string = move(template_string)
node.child_nodes = parse(template_string)
else:
node.child_nodes = []
child_nodes.append(node)
template_string = move(template_string)
return child_nodes
所有在模板中节点,按照父子关系组织起来,是一个森林的数据结构。解析整个模板的过程,就是构造森林的过程。这棵森林所有子树的叶子节点,可以是文本节点,注释节点,表达式节点和块节点中具有表达式意义或者语句意义的节点。而内部节点是宿主节点,但解析遇到一个结束节点,就知道一个宿主节点的所有子节点都解析完成,是时候return。
tornado中对模板的解析,是在template模块中_parse函数。
在web开发,利用模板,目的是通过不同数据渲染相同模板,结果产生针对用户的不同内容页面。整个过程就是一个顶级函数,传入参数两个:一个是模板字符串,另外一个是数据(命名空间),返回的是一个字符串(最终用户在浏览器中看到的内容页面)。
我们在模板字符串中挖的洞,最后需要被实际的数据填补起来。所以关键问题是该如何去填补这些洞。
tornado中的做法非常干脆利落,直接将模板字符串,翻译成一个python函数,然后编译这个python函数,如果web server接受到客户端请求。就直接调用这个编译好的函数,返回的结果就是一个内容页面。
所幸,python语言为tornado模板引擎实现,提供了极大便利——内建函数compile和exec语句。
python中提供了一个内建函数compile,可以编译一个源码字符串成一个code对象,返回的code对象,能够被exec语句或者eval()函数执行。
exec语句支持python代码动态执行。
python2.7中的语法如下:
exec code[ in globals[,locals]]
code可以是一个字符串,一个打开的文件对象或者一个code对象。如果是字符串,会被解析成python语句执行。如果是文件对象,会一直解析到EOF然后执行。如果是code对象,已经不需要解析,可以直接执行。
exec语句也可以看作一个函数(实际上,在python3上,exec就是一个函数),globals(字典结构)和locals(字典结构)都是exec函数的可选参数。如果两者都没有提供,code在当前exec语句的作用域上下文下执行(code中用到全局作用域的对象和局部作用域的对象,并且可以对它们做修改)。
上面提到exec语句执行不提供任何的可选参数,是非常糟糕的,code执行会污染exec语句所在的作用域,并且引发很多难以调试的bug。
globals参数,可以避免这种情况。globals参数,让我们可以自定义code执行的作用域上下文,code执行只能获取globals中提供的变量、函数、类型等等,不过有一种例外—-python中的buildins。在code执行前,exec语句会在globals中,插入一项,key为”_builtins_“。这样code执行可以利用python中所有的buildins(tornado模板中可以利用所用python内建buildins)。
既然code在自定义的作用域上下文执行(相当于在一个沙盒子中),如果我们想在exec语句执行完毕,或者code中定义的类型,变量,函数等等,那该怎么办?
locals参数正是为此而来。
_code = ‘‘‘
def my_output():
print "hello world"
‘‘‘
def main():
compiled = compile(_code, "<string>", "exec")
global_dict = {}
local_dict = {}
exec compiled in global_dict, local_dict
local_dict["my_output"]()
if __name__ == ‘__main__‘:
main()
但是如果locals参数没有提供,只提供了globals参数。exec语句执行完毕,我们还是可以访问到code中定义的变量,类型,函数。exec语句把这一切都插入到了globals字典中。
我们可以看到,当提供了globals和locals参数,exec中的code执行,如果在一个沙盒子中,可以避免对exec语句所在作用域的污染。
还记得在template模块中的_Node基类提供了一个没有实现的方法–generate。每种模板节点都有自己生成代码的方式,generate方法将在子类中实现。
class _File(_Node):
def __init__(self, template, body):
self.template = template
self.body = body
self.line = 0
def generate(self, writer):
writer.write_line("def _tt_execute():", self.line)
with writer.indent():
writer.write_line("_tt_buffer = []", self.line)
writer.write_line("_tt_append = _tt_buffer.append", self.line)
self.body.generate(writer)
writer.write_line("return _tt_utf8(‘‘).join(_tt_buffer)", self.line)
def each_child(self):
return (self.body,)
我们先来看看_File类的generate方法。_File实际上对应的是整个模板文件。那么_File对象调用generate方法返回的结果应该是整个模板文件映射成python代码的结果。这个结果是一个名叫_tt_execute的函数代码字符串。
函数中定义了两个变量。_tt_buffer,一个列表,列表中的每一项将会是各种类型的模板节点映射成python的代码。_tt_append,函数,实际上是_tt_buffer.append函数。
那么_tt_execute函数体的代码如何生成?
由self.body对象调用generate方法。self.body实际上是一个_ChunkList对象。
class _ChunkList(_Node):
def __init__(self, chunks):
self.chunks = chunks
def generate(self, writer):
for chunk in self.chunks:
chunk.generate(writer)
def each_child(self):
return self.chunks
_ChunkList对象是父节点对象的子节点对象列表。
我们看到生成代码的过程,实际是节点森林自顶向下调用generate方法的过程,知道所用的叶子节点的generate方法调用完毕,才自底向上返回。整个过程也遍历了一遍节点森林。
整个过程,很多节点调用generate方法,都是直接拿”{% …%}”中含有的内容就作为生成的python代码。因此tornado的模板语言,具有极大的灵活性,它可以直接用到python语言中的很多语法。个别节点的generate方法生成代码,需要做特殊的处理。
模板继承语法,让生成代码多了一些曲折。在tornado模板语法中,模板继承语法,主要由以下两个指令共同其作用:
指令 | 用法 | 对应_Node子类 |
---|---|---|
block | {% block name %}…{% end %} | _NamedBlock |
extends | {% extends filename %} | _ExtendsBlock |
子模板,继承了一个父模板,父模板的命名块(_NamedBlock)将会被子模板中的命名块(_NamedBlock)替换。
这个过程,如果要生成代码该如何实现?
实现的原理,也很容易可以想到。我们对子模板生成代码,其实不应该调用子模板对应的_File对象的generate方法,反而应该调用的是这个模板继承链最顶级的父模板_File对象的generate方法。
template模块中的Template类,提供了_get_ancestors方法。
def _get_ancestors(self, loader):
ancestors = [self.file]
for chunk in self.file.body.chunks:
if isinstance(chunk, _ExtendsBlock):
if not loader:
raise ParseError("{% extends %} block found, but no "
"template loader")
template = loader.load(chunk.name, self.name)
ancestors.extend(template._get_ancestors(loader))
return ancestors
那么又有另外一个问题,父模板中的命名块节点(_NamedBlock),应该被子模板同名的命名块节点替换,这个又该如何处理?
tornado对这个问题的处理代码如下,思路很简单,就留读者去思考了。
def _generate_python(self, loader, compress_whitespace):
buffer = StringIO()
try:
# named_blocks maps from names to _NamedBlock objects
named_blocks = {}
ancestors = self._get_ancestors(loader)
ancestors.reverse()
for ancestor in ancestors:
ancestor.find_named_blocks(loader, named_blocks)
writer = _CodeWriter(buffer, named_blocks, loader, ancestors[0].template,
compress_whitespace)
ancestors[0].generate(writer)
return buffer.getvalue()
finally:
buffer.close()
第一步首先利用python内建函数compile,将_generate_python返回的python代码字符串编译成code对象。
code对象的执行主要在Template类的中的generate方法。
def generate(self, **kwargs):
namespace = {
"escape": escape.xhtml_escape,
"xhtml_escape": escape.xhtml_escape,
"url_escape": escape.url_escape,
"json_encode": escape.json_encode,
"squeeze": escape.squeeze,
"linkify": escape.linkify,
"datetime": datetime,
"_tt_utf8": escape.utf8, # for internal use
"_tt_string_types": (unicode_type, bytes),
"__name__": self.name.replace(‘.‘, ‘_‘),
"__loader__": ObjectDict(get_source=lambda name: self.code),
}
namespace.update(self.namespace)
namespace.update(kwargs)
exec_in(self.compiled, namespace)
execute = namespace["_tt_execute"]
linecache.clearcache()
return execute()
exec_in是tornado util模块中的一个函数,主要是为了兼容python2和python3(在python2中exec作为语句使用,在python3以后,exec作为函数使用)。
从上面的代码,我们可以看到tornado自定义了一个执行上下文(namespace),提供了一些tornado模板中能够使用的基本函数,如“escape”,“xhtml_escape”等等。当然也会把,generate的参数(用于模板渲染)插入到namespace字典中。_tt_execute函数就在这样的一个namespace环境中执行,最后获得向客户端输出的内容页面字符串。
实际在真正的tornado web开发中,并不需要直接和Template对象打交道,而是通过web模块RequestHandler类的render方法和render_string方法,函数签名如下:
def render_string(self, template_name, **kwargs):
def render(self, template_name, **kwargs):
render_string与render有什么区别?render_string方法是根据一个模板的名字和字典参数,对模板进行渲染,返回的是一个内容页面字符串。而render方法会将渲染的结果,发送到客户端。
我们来看看render_string函数的实现。
def render_string(self, template_name, **kwargs):
template_path = self.get_template_path()
if not template_path:
frame = sys._getframe(0)
web_file = frame.f_code.co_filename
while frame.f_code.co_filename == web_file:
frame = frame.f_back
template_path = os.path.dirname(frame.f_code.co_filename)
with RequestHandler._template_loader_lock:
if template_path not in RequestHandler._template_loaders:
loader = self.create_template_loader(template_path)
RequestHandler._template_loaders[template_path] = loader
else:
loader = RequestHandler._template_loaders[template_path]
t = loader.load(template_name)
namespace = self.get_template_namespace()
namespace.update(kwargs)
return t.generate(**namespace)
render_string函数的作用,根据模板路径和模板名字加载指定的模板,加载模板,创建Template对象,最后调用Template对象的genereate方法。注意到传入generate方法的参数。namespace实际上首先是self.get_template_namespace()函数的返回结果。而get_template_namespace函数的实现如下:
def get_template_namespace(self):
namespace = dict(
handler=self,
request=self.request,
current_user=self.current_user,
locale=self.locale,
_=self.locale.translate,
static_url=self.static_url,
xsrf_form_html=self.xsrf_form_html,
reverse_url=self.reverse_url
)
namespace.update(self.ui)
return namespace
惊喜发现!原来在tornado的模板中还可以用到那么多tornado框架的对象和函数。
本博文主要介绍tornado模板引擎实现原理,包括解析和编译。当然其中也简要介绍了模板语法,模板应用,涉及到数据结构与一些简单算法。tornado模板引擎实现非常简要,整个template模块代码不足千行,但也非常强大。
希望这篇文件对学习tornado模板的人有帮助。也愿意和读者交流学习,可以在csdn我的个人主页找到我的邮件联系方式。如果有转载,请注明来源http://blog.csdn.net/wyx819/article/details/45652713。
原文地址:http://blog.csdn.net/wyx819/article/details/45652713