Monklof思考和写字的地方

Tornado模板系统实现方式

2014-12-19

最近闲来无事突然对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模板系统由两大组件构成:LoaderTemplate

  • 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.htmlbold.htmlbold.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:7
    for 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:11
        pass  # base.html:7
    _tt_append(b'\n</ul>\n</body>\n</html>\n')  # base.html:15
    return _tt_utf8('').join(_tt_buffer)  # base.html:0

执行这段代码,可以获得_tt_execute()这个函数,通过执行这个函数就可以获得渲染后的文本了!

在继续之前,我们先仔细的研究一下生成的这段Python代码。

仔细对比_tt_execute()这个函数和模板文件base.htmlbold.html,会发现很有意思的规则:

  1. 使用_tt_buffer 列表来存放渲染后的文本片段
  2. 使用_tt_append(=_tt_buffer.append)来将渲染后的字符串片段添加进_tt_buffer
  3. 如果一段字符串既不是表达式({{ ... }}),又不是指令({% %}),就直接将这段文本_tt_append,放入_tt_buffer。(无渲染)
  4. 如果是模板表达式语句({{...}}),则会计算表达式结果,然后_tt_append,放入_tt_buffer。(有渲染)
  5. 如果是指令语句({% .. %}),根据指令不同,做出不同的处理。

    1. 如果是对模板本身进行操作的指令类型比如{% block %}, {% extends %}, {% include %}之类的,Tornado会按照指令逻辑本身对模板本身进行相应处理,不会直接生成代码来处理这种逻辑。
    2. 如果是可以直接转换为python代码的语句,比如 {% for %}, {% if %}, {% import .. %}, {% set .. %}之类的,则会被直接编译转为python语句。
  6. 将_tt_buffer中渲染后的字符串片段串接起来,得到最终输出。

这就是Tornado模板系统的秘密所在:按照模板语法,将模板切为一个一个的块(Chunk/Node),然后把块转换为对应逻辑的python代码,最后拼成一个整体的python代码,实现整体渲染逻辑。

比如上面那段编译后的代码是这样得到的:

  1. 编译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>
    
  2. 根据模板语法,会在"!!"的地方把模板切成块(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>
    
  3. 切开之后,就形成了一个一个的块,再把这些块整合成代码。

    比如最开头的文本块:

     <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)
    

    其他块也是类似的原理。

  4. 最后将所有渲染后的字符串片段串接起来,形成最后输出:

         return _tt_utf8('').join(_tt_buffer)  # base.html:0
    

这些工作被Tornado分为如下步骤来完成:

  1. 解析模板内容

    解析模板的目的就是把模板内容根据语法划分成一个一个的"块"。

    从模板最开头开始,依次分割,将模板分解为子"块",在Tornado的实现中,将"块"叫做"结点(Node)"

    结点有很多种类,比如纯文本会被转换为文本结点( _Text ),{{ *expression* }} 会被转换为表达式结点( _Expression),而像{% if *condition* %}, {% while *condition* %}之类的会被转换为控制块结点( _ContronBlock )。

    每个结点都有自己的功能实现代码,比如文本结点无需渲染,那么他实现功能的代码就是:

     _tt_append(TEXT)
    

    表达式结点实现功能的代码:

     _tt_tmp = EXPRESSION
     if 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)
    
  2. 编译解析后的模板,得到python代码。

    解析之后,就可以按顺序得到模板的各个结点了,再依次拼凑各个结点本身的实现代码,包装在 _tt_execute() 这个函数里面就可以得到最终的编译后代码了。

  3. 执行编译后的python代码

    1. 首先执行python代码得到 _tt_execute() 这个函数。
    2. 执行这个函数,就可以渲染后端输出了。

Tornado模板系统代码实现

Tornado模板系统两大组件, LoaderTempalte,都位于Tornado模块 tornado.template 中,其中 Loader 相对较简单,负责加载和管理模板,主要提供了三个“接口”:

  1. class tornado.template.Loader(root_directory, **kwargs)

    创建一个loader实例,可以从root_directory中加载模板文件。

  2. load(name, parent_path=None)

    加载模板文件name,返回 Template 实例

  3. reset()

    清除模板缓存

Template 则提供了两个“接口”:

  1. class tornado.template.Template(template_string, name="<string>", loader=None, compress_whitespace=None, autoescape="xhtml_escape")

    创建一个模板,template_string为需要渲染的字符串

  2. 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 = value
        self.line = line

    def 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 character
        if 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 = expression
        self.line = line
        self.raw = raw

    def 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 directive
        curly = 0
        while True:
            curly = reader.find("{", curly)
            if curly == -1 or curly + 1 == reader.remaining():
                # EOF
                if 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 it
            if reader[curly + 1] not in ("{", "%", "#"):
                curly += 1
                continue
            # 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 meaningful
            if (curly + 2 < reader.remaining() and
                    reader[curly + 1] == '{' and reader[curly + 2] == '{'):
                curly += 1
                continue
            break

        # Append any text before the special token
        if 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

        # Comment
        if 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

        # Expression
        if 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

        # Block
        assert start_brace == "{%", start_brace
        end = 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) blocks
        intermediate_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 tag
        elif operator == "end":
            if not in_block:
                raise ParseError("Extra {% end %} block on line %d" % line)
            return body

        elif operator in ("extends", "include", "set", "import", "from",
                          "comment", "autoescape", "raw", "module"):
            if operator == "comment":
                continue
            if 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 = None
                template.autoescape = fn
                continue
            elif operator == "raw":
                block = _Expression(suffix, line, raw=True)
            elif operator == "module":
                block = _Module(suffix, line)
            body.chunks.append(block)
            continue

        elif operator in ("apply", "block", "try", "if", "for", "while"):
            # parse inner body recursively
            if 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)
            continue

        elif operator in ("break", "continue"):
            if not in_loop:
                raise ParseError("%s outside %s block" % (operator, set(["for", "while"])))
            body.chunks.append(_Statement(contents, line))
            continue

        else:
            raise ParseError("unknown operator: %r" % operator)

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

_parse() 中的reader是Tornado封装的用来操作模板的类 _TemplateReader 实例。 _TemplateReader 是类似于"资源消费者"概念的东西,这里的资源就是模板文本。你可以通过它提供的 consume(count=None) 方法来消费count个字符串:

class _TemplateReader(object):
    def __init__(self, name, text):
        self.name = name
        self.text = text
        self.line = 1
        self.pos = 0

    def find(self, needle, start=0, end=None):
        assert start >= 0, start
        pos = self.pos
        start += pos
        if end is None:
            index = self.text.find(needle, start)
        else:
            end += pos
            assert end >= start
            index = self.text.find(needle, start, end)
        if index != -1:
            index -= pos
        return index

    def consume(self, count=None):
        if count is None:
            count = len(self.text) - self.pos
        newpos = self.pos + count
        self.line += self.text.count("\n", self.pos, newpos)
        s = self.text[self.pos:newpos]
        self.pos = newpos
        return s

    def remaining(self):
        return len(self.text) - self.pos

    def __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.pos
            else:
                start += self.pos
            if stop is not None:
                stop += self.pos
            return 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 objects
            named_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)
            # 最老祖先generate
            ancestors[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 _ChunkList
        self.body = body
        self.line = 0

    def 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 = loader
        try:
            # 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源码里边,我特别想提的两个地方: 代码规范简洁 & 设计

代码规范

  1. 内部使用的类、方法一定会以单个下划线 "_" 开头,这样很方便的区分内部类和方法而快速定位。
  2. 这些内部使用的类一定会放在文件的末尾,方法一定会放在类的末尾,很符合阅读时候的从上而下的逻辑。

设计

设计上:解耦

  1. 将一个模板的各个部分用结点(_Node)来分开,而不是直接处理各个部分的逻辑,拼接各个部分的输出。好处是每个结点的逻辑清晰,当后期扩展指令/修改指令时不会整体的代码,只会影响对应结点。
  2. 将遍历模板字符串的过程抽象出来,用“消费资源”的概念,将其抽象成 _TemplateReader ,这样 _parse 这段解析的代码就可以将注意力集中在解析的逻辑本身,而不必关心字符串处理相关的细节。
  3. 将生成代码逻辑抽象出来,形成 _CodeWriter ,好处是在生成代码的时候,也可以更好的将注意力集中在逻辑本身,而无需在乎这些基础服务。

当一部分的逻辑过于复杂的时候,如何剥离代码逻辑、如何抽象出实体,对我来说,是一个很难的话题。但是在这里,可以看到Tornado做得特别好,值得学习和思考。

PS.

第一次写这种分析源码的文章。写完才发现,很多时候,看这种分析文章还不如直接去看代码来得快,这样的文章是否有价值?

其他发现

threading: a hign lever threading interface

threading.RLock: 一个 可重入锁(reentrant lock)生成器,和一般的锁不同,允许一个线程递归多次获取锁,但是必须释放同样次数才能被其他线程使用。避免单线程递归使用锁时进入死锁状态。

(写于2014-12-30,最近编辑于2015-02-13)

参考

标签: #Explore   #Python  

参与讨论

#1   monklof 2015-07-27 21:11 评论

test

#2   monklof 2015-08-09 10:01 评论

test

评论一下

Captcha