上篇介绍了整个模板引擎的设计思路,这篇我们将讨论整个代码的具体实现,大牛的代码整体思路以及风格非常值得借鉴.
ps:由于代码是本人学习完之后自己敲的,如果有错误之处请指出,code.
CodeBuilder类
该类实现代码的拼接以及解释python代码返回一个
dict
.由于该类的实现参考博客以及说得非常详细了,我这里只说一下我觉得需要讲的地方.add_section
方法,我感觉这个函数设计得非常巧妙,因为当我们用传入的模板生成python代码的时候,我们是不知道全部变量名,所以只能一边分析一边生成变量名.所以我们要保留一块位置放置变量名。该函数生成了一个新的CodeBulider对象在旧的对象中保留了一个参考位置并保留了参考位置的缩进.1234def add_section(self):section = CodeBulider(self.indent_level)self.code.append(section)return sectionget_global
方法,这个方法调用了exec()
,该函数执行一串包含python代码的字符串,它的第二个参数是一个字典,用来收集字符串代码中定义的全局变量。1234567def get_global(self):# 确保整个代码结束后的缩进等级为 0assert self.indent_level == 0code = str(self)namespace = {}exec(code,namespace)return namespace
Templite类
构造函数
该类是模板引擎的核心类,该类构造函数接收两个参数
text
以及*contexts
,星号表示任意数量的位置参数将被打包成一个元组作为contexts传递进来。称为参数解包.可以通过循环遍历取出所有的元素.text
即为传入的模板.编译一个模板为python代码的工作都将在构造器里完成.12345def __init__(self,text=None,*contexts):self.text = textself.contexts = {}for context in contexts:self.contexts.update(context)该类有两个重要的变量,第一个是保存代码中所有出现过的变量,第二个是循环中出现的变量,因为循环中出现过的变量是无需定义成上下文变量的,所以最后在所有变量中除去循环变量即为所需定义的上下文变量:
12345self.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
,为变量名提供了一个参考位置:1234567891011code = 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
,作者在这里做了一个微优化,当缓冲数组长度为一行或者多行时选择不同的方法添加字符串.1234567buffered = []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时控制代码的缩进.关于每一个条件的处理,这里就不一一赘述了,参考博客讲得很明白.
1re.split(r"(?s)({{.*?}}|{%.*?%}|{#.*?#})", text)最后生成的代码以及get_global函数返回的字典分别存在以下两个变量中,至此构造函数完成.
12self.code = codeself.render_function = code.get_global()['render_function']
编译表达式
- 我们前面说过,模板代码里支持三种形式的表达式:一种是普通变量,一种是普通变量加上通过管道连接的过滤器和通过含有点的表达式.
- 对于普通变量,我们只需在前面加上
-c
便可直接返回 - 对于包含管道的表达式,我们分割管道,然后将第一个变量通过递归调用变成代码中所定义的变量,然后将变量与过滤器进行迭代,得到最终结果
- 对于包含点操作符的变量,我们分割点,将第一个变量同样通过递归变成代码中所定义的变量,比如 x.y , 我们可能的结果会有 x[‘y’] , x.y<=>getattr(x,’y’) ,以及可调用三种情况,所以我们将其交给
do_dots()
函数进行处理.12345678910111213141516def 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" % exprreturn code
辅助函数
- 使用
_syntax_error()
来进行异常处理以及validate()
来进行变量名合法性的检测,get_model_code()
用来获取生成的python代码.123456789def _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()
123456789def 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()
,这样就可以得到整个代码的返回值了.1234def 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翻译(下)