本文主要是介绍Tornado模板系统实现方式,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
最近闲来无事突然对tornado模板系统的实现方法很感兴趣,于是花了一些时间仔细的研究了tornado模板系统的实现方法。第一次阅读开源软件的代码,感受:舒心。Tornado的源代码写得很是规范和清晰,钦佩不已。
这篇文章是对Tornado模板系统源码学习的一个总结,主要包含以下几个内容:
- Tornado模板系统简单使用方法
- Tornado模板系统渲染原理
- Tornado模板系统代码实现
Tornado模板系统源码位于tornado.template这个模块里,本文中所研究的tornado版本和测试代码所采用的python版本分别为:
- tornado 4.0.1
- python 3.4
Tornado模板系统简单使用
这里不再赘述Tornado 模板系统的语法了,可以在这里找到:tornado.template — Flexible output generation
Tornado模板系统由两大组件构成:Loader
和Template
- Loader(模板加载器):负责加载和缓存模板文件
- Template(模板):负责解析、编译模板和生成输出
基本用法:
-
方法一:渲染字符串
t = template.Template("<html>{{ myvalue }}</html>")print(t.generate(myvalue="XXX"))
通过Template.generate()方法来生成最终输出
-
方法二:渲染模板文件
loader = template.Loader("/tmp")t = loader.load("test.html")print(t.generate(myvalue="XXX"))
/tmp/test.html:
<html>{{ myvalue }}</html>
通过Loader.load()加载模板文件,返回Template实例
通常,我们编写的模板不会如此简单,现在来一个稍微复杂一点的情况:假设我们有两个在/tmp/
目录下的模板文件bash.html
和 bold.html
,bold.html
extends了base.html
,我们需要渲染bold.html
,得到最终输出。
模板文件内容为:
### base.html
<html><head><title>{% block title %}Default title{% end %}</title></head><body><ul>{% for student in students %}{% block student %}<li>{{ escape(student['name']) }}</li>{% end %}{% end %}</ul></body>
</html>### bold.html
{% extends "base.html" %}
{% block title %}A bolder title{% end %}
{% block student %}<li><span style="bold">{{ escape(student['name']) }}</span></li>
{% end %}
python代码实现方法:
students = [dict(name='david'), dict(name='jack')]
import tornado.template as template
loader = template.Loader("/tmp")
# 加载模板
t = loader.load("bold.html")
# 生成输出
print(t.generate(students=students))
输出结果:
b'<html>\n<head>\n<title>A bolder title</title>\n</head>\n<body>\n<ul>\n\n\n<li><span style="bold">david</span></li>\n\n\n\n<li><span style="bold">jack</span></li>\n\n\n</ul>\n</body>\n</html>\n'
这样,我们得到了预期内的渲染后的html页面。
那么,在Tornado内部是如何渲染文本的呢?
Tornado模板系统渲染原理
整体来说,Tornado模板系统实现渲染的步骤很清晰:
加载模板文件之后,按照语法解析模板并将其编译成python代码,执行python代码得到渲染后的输出。
python代码?没错,Tornado将模板编译成python代码来实现模板渲染,而不是直接解析模板,嵌入数据来得到最终输出。(这一点跟JSP很相似,JSP最后是被转为java代码来实现的。)
比如前面的bold.html
,会被编译成以下代码:
def _tt_execute(): # base.html:0_tt_buffer = [] # base.html:0_tt_append = _tt_buffer.append # base.html:0_tt_append(b'<html>\n<head>\n<title>') # base.html:3_tt_append(b'A bolder title') # bold.html:3 (via base.html:3)_tt_append(b'</title>\n</head>\n<body>\n<ul>\n') # base.html:7for student in students: # base.html:7_tt_append(b'\n') # base.html:8_tt_append(b'\n<li><span style="bold">') # bold.html:6 (via base.html:8)_tt_tmp = escape(student['name']) # bold.html:6 (via base.html:8)if isinstance(_tt_tmp, _tt_string_types): _tt_tmp = _tt_utf8(_tt_tmp) # bold.html:6 (via base.html:8)else: _tt_tmp = _tt_utf8(str(_tt_tmp)) # bold.html:6 (via base.html:8)_tt_tmp = _tt_utf8(xhtml_escape(_tt_tmp)) # bold.html:6 (via base.html:8)_tt_append(_tt_tmp) # bold.html:6 (via base.html:8)_tt_append(b'</span></li>\n') # bold.html:7 (via base.html:8)_tt_append(b'\n') # base.html:11pass # base.html:7_tt_append(b'\n</ul>\n</body>\n</html>\n') # base.html:15return _tt_utf8('').join(_tt_buffer) # base.html:0
执行这段代码,可以获得_tt_execute()
这个函数,通过执行这个函数就可以获得渲染后的文本了!
在继续之前,我们先仔细的研究一下生成的这段Python代码。
仔细对比_tt_execute()
这个函数和模板文件base.html
、bold.html
,会发现很有意思的规则:
- 使用_tt_buffer 列表来存放渲染后的文本片段
- 使用_tt_append(=_tt_buffer.append)来将渲染后的字符串片段添加进_tt_buffer
- 如果一段字符串既不是表达式({{ ... }}),又不是指令({% %}),就直接将这段文本_tt_append,放入_tt_buffer。(无渲染)
- 如果是模板表达式语句({{...}}),则会计算表达式结果,然后_tt_append,放入_tt_buffer。(有渲染)
-
如果是指令语句({% .. %}),根据指令不同,做出不同的处理。
- 如果是对模板本身进行操作的指令类型比如{% block %}, {% extends %}, {% include %}之类的,Tornado会按照指令逻辑本身对模板本身进行相应处理,不会直接生成代码来处理这种逻辑。
- 如果是可以直接转换为python代码的语句,比如 {% for %}, {% if %}, {% import .. %}, {% set .. %}之类的,则会被直接编译转为python语句。
-
将_tt_buffer中渲染后的字符串片段串接起来,得到最终输出。
这就是Tornado模板系统的秘密所在:按照模板语法,将模板切为一个一个的块(Chunk/Node),然后把块转换为对应逻辑的python代码,最后拼成一个整体的python代码,实现整体渲染逻辑。
比如上面那段编译后的代码是这样得到的:
-
编译
bold.html
的时候,发现了这样一个指令:{% extends "base.html" %}
这个时候,Tornado就知道这是继承
base.html
的一个模板,于是加载base.html
,然后替换其中的{% block title%}
和{% block student%}
,而得到了一个完整的模板:<html><head><title>{% block title %}A bolder title{% end %}</title></head><body><ul>{% for student in students %}{% block student %} <li><span style="bold">{{ escape(student['name']) }}</span></li>{% end %}{% end %}</ul></body></html>
-
根据模板语法,会在"!!"的地方把模板切成块(Chunk/Node):
<html><head><title>!!{% block title %}A bolder title{% end %}!!</title></head><body><ul>!!{% for student in students %}!!!!{% block student %}!! <li><span style="bold">!!{{ escape(student['name']) }}!!</span></li>{% end %}{% end %}!!</ul></body></html>
-
切开之后,就形成了一个一个的块,再把这些块整合成代码。
比如最开头的文本块:
<html><head><title>
被直接转换为这一段代码:
_tt_append(b'<html>\n<head>\n<title>') # base.html:3
而指令块
{% for student in students %}
,则直接被转换为python代码:for student in students: # base.html:7
表达式块
{{ escape(student['name']) }}
则会被转化成python代码:_tt_tmp = escape(student.name) # bold.html:6 (via base.html:8)if isinstance(_tt_tmp, _tt_string_types): _tt_tmp = _tt_utf8(_tt_tmp) # bold.html:6 (via base.html:8)else: _tt_tmp = _tt_utf8(str(_tt_tmp)) # bold.html:6 (via base.html:8)_tt_tmp = _tt_utf8(xhtml_escape(_tt_tmp)) # bold.html:6 (via base.html:8)_tt_append(_tt_tmp) # bold.html:6 (via base.html:8)
其他块也是类似的原理。
-
最后将所有渲染后的字符串片段串接起来,形成最后输出:
return _tt_utf8('').join(_tt_buffer) # base.html:0
这些工作被Tornado分为如下步骤来完成:
-
解析模板内容
解析模板的目的就是把模板内容根据语法划分成一个一个的"块"。
从模板最开头开始,依次分割,将模板分解为子"块",在Tornado的实现中,将"块"叫做"结点(Node)"
结点有很多种类,比如纯文本会被转换为文本结点( _Text ),{{ *expression* }} 会被转换为表达式结点(_Expression),而像{% if *condition* %}, {% while *condition* %}之类的会被转换为控制块结点(_ContronBlock )。
每个结点都有自己的功能实现代码,比如文本结点无需渲染,那么他实现功能的代码就是:
_tt_append(TEXT)
表达式结点实现功能的代码:
_tt_tmp = EXPRESSIONif isinstance(_tt_tmp, _tt_string_types): _tt_tmp = _tt_utf8(_tt_tmp)else: _tt_tmp = _tt_utf8(str(_tt_tmp))_tt_tmp = _tt_utf8(xhtml_escape(_tt_tmp))_tt_append(_tt_tmp)
-
编译解析后的模板,得到python代码。
解析之后,就可以按顺序得到模板的各个结点了,再依次拼凑各个结点本身的实现代码,包装在 _tt_execute()这个函数里面就可以得到最终的编译后代码了。
-
执行编译后的python代码
- 首先执行python代码得到 _tt_execute() 这个函数。
- 执行这个函数,就可以渲染后端输出了。
Tornado模板系统代码实现
Tornado模板系统两大组件, Loader 和 Tempalte,都位于Tornado模块 tornado.template 中,其中 Loader 相对较简单,负责加载和管理模板,主要提供了三个“接口”:
-
class tornado.template.Loader(root_directory, **kwargs)
创建一个loader实例,可以从root_directory中加载模板文件。
-
load(name, parent_path=None)
加载模板文件name,返回 Template 实例
-
reset()
清除模板缓存
Template 则提供了两个“接口”:
-
class tornado.template.Template(template_string, name="<string>", loader=None, compress_whitespace=None, autoescape="xhtml_escape")
创建一个模板,template_string为需要渲染的字符串
-
generate(**kwargs)
渲染模板,返回渲染后文本。
Loader 部分的实现相对简单,这里不进行分析,有兴趣的同志可以在这里看实现代码:http://www.tornadoweb.org/en/stable/_modules/tornado/template.html#Loader
解析
前面提过,解析的目的在于将模板转换为一个一个的结点,每个结点都有实现自己功能的代码。
在实现中,结点抽象成基类 _Node 来表示, _Node 通过提供generate()
方法来生成该结点的python代码:
class _Node(object):def each_child(self):return ()def generate(self, writer):"""生成该Node块代码"""raise NotImplementedError()def find_named_blocks(self, loader, named_blocks):for child in self.each_child():child.find_named_blocks(loader, named_blocks)
其中的 each_child() 和 find_named_blocks() 这两个接口暂时先不研究。
具体的结点会继承 _Node 来提供实现自己的功能代码。例如文本节点 _Text:
class _Text(_Node):def __init__(self, value, line):""" @value: 文本内容@line:该文本在模板中的行数"""self.value = valueself.line = linedef generate(self, writer):value = self.value# Compress lots of white space to a single character. If the whitespace# breaks a line, have it continue to break a line, but just with a# single \n characterif writer.compress_whitespace and "<pre>" not in value:value = re.sub(r"([\t ]+)", " ", value)value = re.sub(r"(\s*\n\s*)", "\n", value)if value:writer.write_line('_tt_append(%r)' % escape.utf8(value), self.line)
注: writer是Tornado用来生成python代码的一个工作,可以通过writer.write_line(code)来向最终代码中添加一行代码。
它的 generate() 生成代码方法最简单,如果设置为需要压缩空白的话,就将文本压缩一下,无需的话,就直接将这行代码写入总代码中去了:
'_tt_append(%r)' % escape.utf8(value)
表达式结点也是类似的原理,只不过生成代码的内容不一样:
class _Expression(_Node):def __init__(self, expression, line, raw=False):self.expression = expressionself.line = lineself.raw = rawdef generate(self, writer):writer.write_line("_tt_tmp = %s" % self.expression, self.line)writer.write_line("if isinstance(_tt_tmp, _tt_string_types):"" _tt_tmp = _tt_utf8(_tt_tmp)", self.line)writer.write_line("else: _tt_tmp = _tt_utf8(str(_tt_tmp))", self.line)if not self.raw and writer.current_template.autoescape is not None:# In python3 functions like xhtml_escape return unicode,# so we have to convert to utf8 again.writer.write_line("_tt_tmp = _tt_utf8(%s(_tt_tmp))" %writer.current_template.autoescape, self.line)writer.write_line("_tt_append(_tt_tmp)", self.line)
解析的工作就是从模板开头开始,根据语法寻找指令,将根据模板指令不同,生成对应的结点,大体算法:
(注:指令是指Tornado模板语法中的{{...}}, {% ... %}之类的标识着Tornado特殊语法指令)
Step 1. 生成一个body = _ChunkList
用来保存解析结果,将当前位置设为模板开头 Step 2. 从当前位置开始,往后搜寻模板指令 Step 3. 如果有下一个指令, 进入Step 4;如果没有下一个指令,进入Step 5 Step 4. 将到此为止的所有文本转换为_Text结点,append到body中。判断指令类型,将该指令转换为对应结点,append到body中。跳到Step 2 Step 5. 直接将到此为止的文本转换为_Text结点,append到body中,结束
代码:
def _parse(reader, template, in_block=None, in_loop=None):body = _ChunkList([])while True:# Find next template directivecurly = 0while True:curly = reader.find("{", curly)if curly == -1 or curly + 1 == reader.remaining():# EOFif in_block:raise ParseError("Missing {% end %} block for %s" %in_block)body.chunks.append(_Text(reader.consume(), reader.line))return body# If the first curly brace is not the start of a special token,# start searching from the character after itif reader[curly + 1] not in ("{", "%", "#"):curly += 1continue# When there are more than 2 curlies in a row, use the# innermost ones. This is useful when generating languages# like latex where curlies are also meaningfulif (curly + 2 < reader.remaining() andreader[curly + 1] == '{' and reader[curly + 2] == '{'):curly += 1continuebreak# Append any text before the special tokenif curly > 0:cons = reader.consume(curly)body.chunks.append(_Text(cons, reader.line))start_brace = reader.consume(2)line = reader.line# Template directives may be escaped as "{{!" or "{%!".# In this case output the braces and consume the "!".# This is especially useful in conjunction with jquery templates,# which also use double braces.if reader.remaining() and reader[0] == "!":reader.consume(1)body.chunks.append(_Text(start_brace, line))continue# Commentif start_brace == "{#":end = reader.find("#}")if end == -1:raise ParseError("Missing end expression #} on line %d" % line)contents = reader.consume(end).strip()reader.consume(2)continue# Expressionif start_brace == "{{":end = reader.find("}}")if end == -1:raise ParseError("Missing end expression }} on line %d" % line)contents = reader.consume(end).strip()reader.consume(2)if not contents:raise ParseError("Empty expression on line %d" % line)body.chunks.append(_Expression(contents, line))continue# Blockassert start_brace == "{%", start_braceend = reader.find("%}")if end == -1:raise ParseError("Missing end block %} on line %d" % line)contents = reader.consume(end).strip()reader.consume(2)if not contents:raise ParseError("Empty block tag ({% %}) on line %d" % line)operator, space, suffix = contents.partition(" ")suffix = suffix.strip()# Intermediate ("else", "elif", etc) blocksintermediate_blocks = {"else": set(["if", "for", "while", "try"]),"elif": set(["if"]),"except": set(["try"]),"finally": set(["try"]),}allowed_parents = intermediate_blocks.get(operator)if allowed_parents is not None:if not in_block:raise ParseError("%s outside %s block" %(operator, allowed_parents))if in_block not in allowed_parents:raise ParseError("%s block cannot be attached to %s block" % (operator, in_block))body.chunks.append(_IntermediateControlBlock(contents, line))continue# End tagelif operator == "end":if not in_block:raise ParseError("Extra {% end %} block on line %d" % line)return bodyelif operator in ("extends", "include", "set", "import", "from","comment", "autoescape", "raw", "module"):if operator == "comment":continueif operator == "extends":suffix = suffix.strip('"').strip("'")if not suffix:raise ParseError("extends missing file path on line %d" % line)block = _ExtendsBlock(suffix)elif operator in ("import", "from"):if not suffix:raise ParseError("import missing statement on line %d" % line)block = _Statement(contents, line)elif operator == "include":suffix = suffix.strip('"').strip("'")if not suffix:raise ParseError("include missing file path on line %d" % line)block = _IncludeBlock(suffix, reader, line)elif operator == "set":if not suffix:raise ParseError("set missing statement on line %d" % line)block = _Statement(suffix, line)elif operator == "autoescape":fn = suffix.strip()if fn == "None":fn = Nonetemplate.autoescape = fncontinueelif operator == "raw":block = _Expression(suffix, line, raw=True)elif operator == "module":block = _Module(suffix, line)body.chunks.append(block)continueelif operator in ("apply", "block", "try", "if", "for", "while"):# parse inner body recursivelyif operator in ("for", "while"):block_body = _parse(reader, template, operator, operator)elif operator == "apply":# apply creates a nested function so syntactically it's not# in the loop.block_body = _parse(reader, template, operator, None)else:block_body = _parse(reader, template, operator, in_loop)if operator == "apply":if not suffix:raise ParseError("apply missing method name on line %d" % line)block = _ApplyBlock(suffix, line, block_body)elif operator == "block":if not suffix:raise ParseError("block missing name on line %d" % line)block = _NamedBlock(suffix, block_body, template, line)else:block = _ControlBlock(contents, line, block_body)body.chunks.append(block)continueelif operator in ("break", "continue"):if not in_loop:raise ParseError("%s outside %s block" % (operator, set(["for", "while"])))body.chunks.append(_Statement(contents, line))continueelse:raise ParseError("unknown operator: %r" % operator)class _ChunkList(_Node):def __init__(self, chunks):self.chunks = chunksdef generate(self, writer):for chunk in self.chunks:chunk.generate(writer)def each_child(self):return self.chunks
_parse() 中的reader是Tornado封装的用来操作模板的类 _TemplateReader 实例。 _TemplateReader 是类似于"资源消费者"概念的东西,这里的资源就是模板文本。你可以通过它提供的 consume(count=None) 方法来消费count个字符串:
class _TemplateReader(object):def __init__(self, name, text):self.name = nameself.text = textself.line = 1self.pos = 0def find(self, needle, start=0, end=None):assert start >= 0, startpos = self.posstart += posif end is None:index = self.text.find(needle, start)else:end += posassert end >= startindex = self.text.find(needle, start, end)if index != -1:index -= posreturn indexdef consume(self, count=None):if count is None:count = len(self.text) - self.posnewpos = self.pos + countself.line += self.text.count("\n", self.pos, newpos)s = self.text[self.pos:newpos]self.pos = newposreturn sdef remaining(self):return len(self.text) - self.posdef __len__(self):return self.remaining()def __getitem__(self, key):if type(key) is slice:size = len(self)start, stop, step = key.indices(size)if start is None:start = self.poselse:start += self.posif stop is not None:stop += self.posreturn self.text[slice(start, stop, step)]elif key < 0:return self.text[key]else:return self.text[self.pos + key]def __str__(self):return self.text[self.pos:]
这是一个很优美的存在 :)
编译
如果读者你仔细看了上文,并且我说明白了你看懂了的话,你可能会觉得编译应该是个很简单的事情:各个结点都可以生成自己的代码了,直接组合一下就Ok了。
有一个特殊的情况,就是 {% extends *filename* %} 这个指令,这个指令的功能:
该指令所在的模板继承自filename, 用该模板中的{% block ...%} 标签内容去替换父模板filename中的相应{% block %}内容来达到扩展的目的。该模块中除了{% block %}标签内容以外,其他的内容都会被忽略。
所以,当Tornado编译一个模板时,如果在开头遇到了这个标签,必须得先加载父模板,然后替换这些{% block %} 标签块。假如父模板又用到了父模板(祖先模板)怎么办?根据规则,以最祖先的模板为基本模板,用他的孩子、孙子、曾孙...中的{% block %}标签块内容替换原来的{% block %}块。
在Tornado实现时,{% block %}标签块结点用 _NamedBlock类来表示,具体代码:
class Template(object):...def _generate_python(self, loader, compress_whitespace):buffer = StringIO()try:# named_blocks maps from names to _NamedBlock objectsnamed_blocks = {}# 获取该模板继承(extends)的一些模板, ancestor是_File的实例ancestors = self._get_ancestors(loader)ancestors.reverse()# 按照继承链从祖先往子孙找出所有的{% block %}块(_NamedBlock),存放至named_block中for ancestor in ancestors:ancestor.find_named_blocks(loader, named_blocks)writer = _CodeWriter(buffer, named_blocks, loader, ancestors[0].template,compress_whitespace)# 最老祖先generateancestors[0].generate(writer)return buffer.getvalue()finally:buffer.close()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
其中的 self.file 就是存放各个结点的模板容器,它存放了所有的结点,并且将各个结点的代码串接起来,包装在函数_tt_execute() 中得到最终生成的python代码:
class _File(_Node):"""模板文件,最后生成代码"""def __init__(self, template, body):self.template = template# an instance of _ChunkListself.body = bodyself.line = 0def generate(self, writer):"""_tt_execute(): 运行时渲染的函数_tt_buffer=[]: 单Node渲染后的结果字符串列表_tt_append = _tt_buffer.append"""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,)
(其中的self.body就是 _parse 的结果)
至此,Python代码终于编译好了。
解析和编译这两个步骤,全部都是在生成 Template 的时候完成的:
class Template(object):...def __init__(self, template_string, name="<string>", loader=None,compress_whitespace=None, autoescape=_UNSET):reader = _TemplateReader(name, escape.native_str(template_string))# 解析模板self.file = _File(self, _parse(reader, self))# 生成的python代码self.code = self._generate_python(loader, compress_whitespace)self.loader = loadertry:# Under python2.5, the fake filename used here must match# the module name used in __name__ below.# The dont_inherit flag prevents template.py's future imports# from being applied to the generated code.self.compiled = compile(escape.to_unicode(self.code),"%s.generated.py" % self.name.replace('.', '_'),"exec", dont_inherit=True)except Exception:formatted_code = _format_code(self.code).rstrip()app_log.error("%s code:\n%s", self.name, formatted_code)raise
接下来,通过调用 Template.generate(kwargs) 就可以获得最终渲染后的输出了。
执行python代码
这段直接来上代码就好了:
class Template(object):...def generate(self, **kwargs):"""Generate this template with the given arguments."""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_type),# __name__ and __loader__ allow the traceback mechanism to find# the generated source code."__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"]# Clear the traceback module's cache of source data now that# we've generated a new template (mainly for this module's# unittests, where different tests reuse the same name).linecache.clearcache()return execute()
我第一次看到这里的时候,眼前一亮,我终于知道Tornado模板中那些内置的函数是什么原理了!看这一句:
exec_in(self.compiled, namespace)
exec_in是什么!?
它是Tornado包装Python exec 语句的一个函数:
def exec_in(code, glob, loc=None):if isinstance(code, str):code = compile(code, '<string>', 'exec', dont_inherit=True)exec(code, glob, loc)
亮点来了!exec(code, glob, loc)这个函数的魔法:
在程序内执行这段code代码,使用glob作为它的全局变量容器,loc作为它的本地变量容器! 也就是说,你可以给这段代码制定运行前的变量(glob),同时可以获得他的运行结果(loc)! 当loc = None 时,执行code之后生成的变量会直接保存在glob中。
那么回头看 Template.generate(\kwargs) 代码,我们可以通过给 exec_in 这个函数传递全局名字空间而实现"内置变量"的感觉!像Tornado模板内置的 escape / linkify 函数,还有在 RequestHandler 中的 render()* 方法中传递的模板参数, 都是这个时候传进去的!
神奇的魔法,Python居然还可以这么用,我第一次看见这种用法的时候这样想。
总结 & 阅读感想
Tornado是我读过的第一个开源软件,是一个特别棒的软件。他的棒不仅仅是在代码注释、代码风格以及各种技术的恰当的使用上,更在它的整个软件的设计上。很多的设计,我觉得很美妙,可惜笨拙如我,没法用文字描述出来 TAT。
Tornado源码里边,我特别想提的两个地方: 代码规范简洁 & 设计
代码规范
- 内部使用的类、方法一定会以单个下划线 "_" 开头,这样很方便的区分内部类和方法而快速定位。
- 这些内部使用的类一定会放在文件的末尾,方法一定会放在类的末尾,很符合阅读时候的从上而下的逻辑。
设计
设计上:解耦:
- 将一个模板的各个部分用结点(_Node)来分开,而不是直接处理各个部分的逻辑,拼接各个部分的输出。好处是每个结点的逻辑清晰,当后期扩展指令/修改指令时不会整体的代码,只会影响对应结点。
- 将遍历模板字符串的过程抽象出来,用“消费资源”的概念,将其抽象成 _TemplateReader ,这样 _parse 这段解析的代码就可以将注意力集中在解析的逻辑本身,而不必关心字符串处理相关的细节。
- 将生成代码逻辑抽象出来,形成 _CodeWriter ,好处是在生成代码的时候,也可以更好的将注意力集中在逻辑本身,而无需在乎这些基础服务。
当一部分的逻辑过于复杂的时候,如何剥离代码逻辑、如何抽象出实体,对我来说,是一个很难的话题。但是在这里,可以看到Tornado做得特别好,值得学习和思考。
PS.
第一次写这种分析源码的文章。写完才发现,很多时候,看这种分析文章还不如直接去看代码来得快,这样的文章是否有价值?
其他发现
threading: a hign lever threading interface
threading.RLock: 一个 可重入锁(reentrant lock)生成器,和一般的锁不同,允许一个线程递归多次获取锁,但是必须释放同样次数才能被其他线程使用。避免单线程递归使用锁时进入死锁状态。
(写于2014-12-30,最近编辑于2015-02-13)
这篇关于Tornado模板系统实现方式的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!