Fate

(500 lines or less)利用python做一个模板引擎(下)

Markdown

上篇介绍了整个模板引擎的设计思路,这篇我们将讨论整个代码的具体实现,大牛的代码整体思路以及风格非常值得借鉴.
ps:由于代码是本人学习完之后自己敲的,如果有错误之处请指出,code.

CodeBuilder类

  • 该类实现代码的拼接以及解释python代码返回一个dict.由于该类的实现参考博客以及说得非常详细了,我这里只说一下我觉得需要讲的地方.

  • add_section方法,我感觉这个函数设计得非常巧妙,因为当我们用传入的模板生成python代码的时候,我们是不知道全部变量名,所以只能一边分析一边生成变量名.所以我们要保留一块位置放置变量名。该函数生成了一个新的CodeBulider对象在旧的对象中保留了一个参考位置并保留了参考位置的缩进.

    1
    2
    3
    4
    def add_section(self):
    section = CodeBulider(self.indent_level)
    self.code.append(section)
    return section
  • get_global方法,这个方法调用了exec(),该函数执行一串包含python代码的字符串,它的第二个参数是一个字典,用来收集字符串代码中定义的全局变量。

    1
    2
    3
    4
    5
    6
    7
    def get_global(self):
    # 确保整个代码结束后的缩进等级为 0
    assert self.indent_level == 0
    code = str(self)
    namespace = {}
    exec(code,namespace)
    return namespace

Templite类

构造函数

  • 该类是模板引擎的核心类,该类构造函数接收两个参数text以及*contexts,星号表示任意数量的位置参数将被打包成一个元组作为contexts传递进来。称为参数解包.可以通过循环遍历取出所有的元素.text即为传入的模板.编译一个模板为python代码的工作都将在构造器里完成.

    1
    2
    3
    4
    5
    def __init__(self,text=None,*contexts):
    self.text = text
    self.contexts = {}
    for context in contexts:
    self.contexts.update(context)
  • 该类有两个重要的变量,第一个是保存代码中所有出现过的变量,第二个是循环中出现的变量,因为循环中出现过的变量是无需定义成上下文变量的,所以最后在所有变量中除去循环变量即为所需定义的上下文变量:

    1
    2
    3
    4
    5
    self.all_vars = set()
    self.loop_vars = set()
    #中间代码省略
    for var_name in self.all_vars - self.loop_vars:
    vars_code.add_line('c_%s = context[%r]'%(var_name,var_name))
  • 使用CodeBulider组建好python代码,这里最值得注意的是vars_code,为变量名提供了一个参考位置:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    code = CodeBulider()
    code.add_line('def render_function(context, do_dots):')
    code.add_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')
    #中间代码省略
    code.add_line('return "".join(result)')
    code.sub_indent()
  • 缓冲区buffered,作者在这里做了一个微优化,当缓冲数组长度为一行或者多行时选择不同的方法添加字符串.

    1
    2
    3
    4
    5
    6
    7
    buffered = []
    def flush_output():
    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[:] #清除缓冲数组
  • 利用正则表达式匹配出我们需要解析的元素.并且一一对这些元素进行分析.然后利用一个操作符栈,在碰到if或者for时控制代码的缩进.关于每一个条件的处理,这里就不一一赘述了,参考博客讲得很明白.

    1
    re.split(r"(?s)({{.*?}}|{%.*?%}|{#.*?#})", text)
  • 最后生成的代码以及get_global函数返回的字典分别存在以下两个变量中,至此构造函数完成.

    1
    2
    self.code = code
    self.render_function = code.get_global()['render_function']

编译表达式

  • 我们前面说过,模板代码里支持三种形式的表达式:一种是普通变量,一种是普通变量加上通过管道连接的过滤器和通过含有点的表达式.
  • 对于普通变量,我们只需在前面加上-c便可直接返回
  • 对于包含管道的表达式,我们分割管道,然后将第一个变量通过递归调用变成代码中所定义的变量,然后将变量与过滤器进行迭代,得到最终结果
  • 对于包含点操作符的变量,我们分割点,将第一个变量同样通过递归变成代码中所定义的变量,比如 x.y , 我们可能的结果会有 x[‘y’] , x.y<=>getattr(x,’y’) ,以及可调用三种情况,所以我们将其交给do_dots()函数进行处理.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    def expr_code(self,expr):
    if '|' in expr:
    words = expr.split('|')
    code = self.expr_code(words[0])
    for func in words[1:]:
    self.validate(func,self.all_vars)
    code = "c_%s(%s)"%(func,code)
    elif '.' in expr:
    words = expr.split('.')
    code = self.expr_code(words[0])
    args = ",".join(repr(c) for c in words[1:])
    code = "do_dots(%s,%s)" % (code,args)
    else:
    self.validate(expr,self.all_vars)
    code = "c_%s" % expr
    return code

辅助函数

  • 使用_syntax_error()来进行异常处理以及validate()来进行变量名合法性的检测,get_model_code()用来获取生成的python代码.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    def _syntax_error(self, msg, thing):
    raise TempliteError("%s: %r" % (msg, thing))
    def validate(self,word,var_set):
    if not re.match(r'[_a-zA-Z][_0-9a-zA-Z]*$',word):
    self._syntax_error("var_name is invalid",word)
    var_set.add(word) #注意 set 是用 add 方法添加元素
    def get_model_code(self):
    return self.code

do_dots()函数

  • 该方法也就是进行枚举,举个例子,product.name.upper(这里的upper代表upper,假设product是一个字典,name是其元素),最后的结果就是 product['name'].upper()
    1
    2
    3
    4
    5
    6
    7
    8
    9
    def do_dots(self,expr,*args):
    for word in args:
    try:
    expr = expr[word]
    except:
    expr = getattr(expr,word)
    if callable(expr):
    expr = expr()
    return expr

渲染

  • 这里为什么叫渲染我也不大清楚,但是一时也想不到比较好的名字来形容这个函数.将新传入的context(这个context里面包含的一般是模板变量的值)与之前的context(这里的context一般是模板中的方法重新定义的名字)合并,并且将do_dots()作为参数传入render_function(),这样就可以得到整个代码的返回值了.
    1
    2
    3
    4
    def render(self,context=None):
    if context:
    self.contexts.update(context)
    return self.render_function(self.contexts,self.do_dots)

后记

代码的功能不是很强,可以自己慢慢完善,代码风格是非常值得学习的.作者留给我们的扩展:

  • 模板继承和包含
  • 自定义标签
  • 自动换码
  • 参数过滤器
  • 复杂条件逻辑如else和elif
  • 不止一个循环变量的循环
  • 空白的控制

参考博客

从零开始一个模板引擎的python实现——500 lines or less-A Template Engine翻译(下)

热评文章