本课程由ekCit发布在实验楼,完整教程及在线练习地址:Python实现模板引擎
一、课程介绍
1. 内容简介
模版引擎使得用户界面能够与业务数据分离,前端与后端分离,它通常用于渲染页面文件。本课程将使用Python实现一个具备基础功能的模板引擎。
2. 课程知识点
本课程项目完成过程中,我们将学习:实现模版引擎的原理与方法
如何编写程序生成代码
3. 课程来源
本课程核心部分来自《500 lines or less》项目,作者是来自 edX 的工程师 Ned Batchelder,这是他的博客:http://nedbatchelder.com/ 。项目代码使用 MIT 协议,项目文档使用 http://creativecommons.org/licenses/by/3.0/legalcode 协议。课程内容在原文档基础上做了稍许修改,增加了部分原理介绍,步骤的拆解分析及源代码注释。
二、实验环境
打开终端,进入 Code 目录,创建 template-engine 文件夹, 并将其作为我们的工作目录。
$ cd Code
$ mkdir template-engine && cd template-engine
三、实验原理
模板介绍
大部分程序都存在大量的逻辑设计和少部分的文字数据,编程语言为此而生。但有时候也会遇到只需要少部分逻辑设计但却要处理大量文字数据的情况,对于这类,还是有一个专门的工具来处理比较好,模板引擎为此而生。
以Web应用为例,它在服务器端生成html页面由客户端的浏览器解析渲染。因为大量的页面存在用户名、商品列表、朋友圈动态等动态数据,所以就需要程序动态地生成页面。并且我们也希望静态文本(html标记)的部分能够完全交由前端设计师完成,后端只用专心负责动态文本的生成。
先从一个简单的例子开始,我们想要生成以下HTML文本:
Welcome, Charlie!
Products:
Apple: $1.00 Fig: $1.50 Pomegranate: $3.25其中出现的动态文本有用户姓名,商品名,商品售价。同时商品种类的数量也并不是固定的。
为了生成这段HTML文本,我们将静态文本作为字符串常量存储,动态文本通过format格式化进字符串:
# 页面的主要文本,其中name和products是动态的部分
PAGE_HTML = """
Welcome, {name}!
Products:
{products}
"""
# 商品项的的主要文本,prodname与price是动态部分
PRODUCT_HTML = "
{prodname}: {price}\n"def make_page(username, products):
#存储商品列表文本
product_html = ""
for prodname, price in products:
product_html += PRODUCT_HTML.format(
prodname=prodname, price=format_price(price))
html = PAGE_HTML.format(name=username, products=product_html)
return html
虽然这段代码奏效了,但是看上去有点混乱。静态文本被分成了PAGE_HTML与PRODUCT_HTML两部分,而且动态数据格式化的细节操作都在Python代码中,使得前端设计师不得不去修改Python文件。随着代码量的增多,这段代码也会渐渐变得难以维护。
更好的做法是使用模板,整个html文件就可以作为一个模板,需要动态文本填充的部分就使用标签标识。
模板化后的模板文件(html文件):
Welcome, {{user_name}}!
Products:
{% for product in product_list %}
{{ product.name }}:{{ product.price|format_price }}
{% endfor %}
对比之前的代码是HTML文本嵌在Python代码中,现在的代码是把HTML文本的部分完全拿了出来而在其中嵌了少许逻辑。
我们知道字符串可以通过format函数将数据代入,而模板将format的功能进行了扩展,它可以支持条件判断与循环等逻辑,因此模版也可以看作是format的高级版吧。
想要在程序中使用HTML模板,首先需要一个模板引擎,模版引擎能够结合HTML模板与上下文环境(包括准备导入的数据等)生成完整的html页面,它的工作就是解析模板,将其中动态的部分与数据进行替换。
当然模板引擎不一定非用在生成html页面上,它就是一个生成文本的引擎,用在什么类型的文本上都是合适的。
模板使用的语法
模板引擎因其支持的语法而异,本课程中使用的引擎语法基于Django - 一个非常流行的web框架。
代入数据使用双花括号:
Welcome, {{user_name}}!
有时候我们希望传入一个对象或者词典,那在模板中要怎么取得对象的属性或者词典的键值呢。
如果是在Python中,取得的语法都是不一样的。
dict["key"]
obj.attr
obj.method()
而在模版中,则是统一使用点:
dict.key
obj.attr
obj.method
用例如下:
The price is: {{product.price}}, with a {{product.discount}}% discount.
你可以使用过滤器过滤修改数据,过滤器使用管道符号调用,用例如下:
Short name: {{story.subject|slugify|lower}}
在模板中使用条件判断:
{% if user.is_logged_in %}
Welcome, {{ user.name }}!
{% endif %}
在模板中使用for循环:
Products:
{% for product in product_list %}
{{ product.name }}: {{ product.price|format_price }}{% endfor %}
就像一般的程序一样,判断与循环可以嵌套组成更复杂的逻辑。
在模版中使用注释:
{# This is the best template ever! #}
引擎的实现方法
大方向上,模板的处理流程可以分为两部分:解析阶段与渲染阶段。
渲染模板需要考虑以下几方面:管理数据来源(即上下文环境)
处理逻辑(条件判断、循环)的部分
实现点取得成员属性或者键值的功能、实现过滤器调用
问题的关键在于从解析阶段到渲染阶段是如何过渡的。解析得到了什么?渲染又是在渲染什么?解析阶段可能有两种不同的做法:解释或者是编译,这正对应了我们的程序语言的实现方式。
在解释型模型中,解析阶段最后会生成能够反映模板结构的数据结构。渲染阶段会遍历整个数据结构并基于预设的指令集生成最后的结果文本。Django使用的模板引擎使用的就是这种方式。
在编译模型中,解析阶段最后会生成某种可直接运行的代码。渲染阶段可直接运行代码得到结果文本。Jinja2与Mako就是使用这种方式的两个典型。
我们使用第二种,也就是编译的方式来实现我们的模板引擎:我们会先将模板编译成Python代码,然后再通过运行这段代码生成结果文本。
编译生成Python函数
先来看一个从模板到Python函数的例子,还是拿之前的例子举例。
模版文本:
Welcome, {{user_name}}!
Products:
{% for product in product_list %}
{{ product.name }}:{{ product.price|format_price }}
{% endfor %}
模板编译后生成的Python函数:
def render_function(context, do_dots):
c_user_name = context['user_name']
c_product_list = context['product_list']
c_format_price = context['format_price']
result = []
append_result = result.append
extend_result = result.extend
to_str = str
extend_result([
'
Welcome, ',
to_str(c_user_name),
'!
\nProducts:
\n- \n'
])
for c_product in c_product_list:
extend_result([
'\n
',to_str(do_dots(c_product, 'name')),
':\n ',
to_str(c_format_price(do_dots(c_product, 'price'))),
'
\n'])
append_result('\n
\n')return ''.join(result)
每一个模版都会被转换为render_function函数,其中context上下文环境存储导入的数据词典。do_dots存储用来取得对象属性或者词典键值的函数。
我们从头开始分析这段代码,最开始是对输入的数据词典进行解包,得到的每个变量都使用c_作为前缀。先使用队列来存储结果,append与extend可能会在在代码中多次用到,所以使用append_result与extend_result来引用它们,这样会比平时直接使用append少一次检索的开销。之后就是使用append_result与extend_result把结果串起来,其中需要替换的部分就用输入数据替换,最后把队列合成一个字符串作为结果文本返回。
使用变量引用append来得到一点点性能上的优化(微优化)可以算是某种奇技淫巧,它是以牺牲可读性为代价的,新手程序员尽量不要在这类型的优化上花太多功夫,这类优化只推荐在已经被前人证明确实能够提升性能且是有益的情况下使用。
你可能还注意到了to_str = str这句,它也是一种微优化,Python检索局部空间比检索内置空间早,所以把str存储在局部变量中也是一种优化。
四、实验步骤
本项目的详细步骤和代码详解,可在实验楼中查看并在线完成,立即【开始实验】
更多Python经典项目:Python全部 - 课程