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

500 lines or less 是一系列非常经典而相对短小的python文章,每一章代码不超过500行,却实现了一些强大的功能,由业内大牛执笔,有很大的学习价值。适合新手了解基本概念,也适合用来python进阶。
本篇原文
源码
其他的一些开源的翻译文章

引入

某些编程任务中,逻辑很少但是文本内容很多。对于这些任务,我们希望有一个更好的工具来解决这些文字为主的问题。模板引擎就是这样一个工具。在这篇文章中,我们建立了一个简单的模板引擎。

Web应用程序是以文字为主的任务的最常见例子。在任何Web应用程序的一个重要阶段就是生成HTML送达至浏览器。只有很少的HTML页面是纯静态的,它们基本上至少含有一小点动态数据,例如用户名。通常它们含有更多的动态数据:产品列表,朋友的新消息等等。

同时,每个HTML页面含有大片静态文本。并且这些页面都很庞大,包含文本的字节数以万计。那么,Web应用程序开发者面临一个问题:怎样生成一个静态和动态数据混合的大型字符串是最好的?此外,静态文本内容实际上是HTML标记语言,由团队中的其他成员——前端设计师创造,这种生成方式最好是他们熟悉的。

为了说明,我们假设要生成这种极简的HTML:

<p>Welcome, Charlie!</p>
<p>Products:</p>
<ul>
  <li>Apple: $1.00</li>
  <li>Fig: $1.50</li>
  <li>Pomegranate: $3.25</li>
</ul>

在这里,用户名将是动态的,产品的名称和价格也将是动态的。甚至产品的数量也是不固定的,因为库存是变动的。
生成HTML的一种方式是在我们的代码中增加字符串常量,再将它们和动态数据结合在一起来产生页面 。动态数据将以某种字符串替换的形式插入。我们的某些动态数据的展现形式是重复的,比如我们的产品列表,这意味着我们有一批重复的HTML块。所以我们将它单独处理再与其它部分组合。
以上述方式生成页面将是这样的:

# The main HTML for the whole page.
PAGE_HTML = """
<p>Welcome, {name}!</p>
<p>Products:</p>
<ul>
{products}
</ul>
"""

# The HTML for each product displayed.
PRODUCT_HTML = "<li>{prodname}: {price}</li>\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

它能工作,但是给我们增加了很多麻烦。HTML代码在多个字符串常量里,嵌入在应用代码中。页面的逻辑很不清晰,因为静态内容被分成了几片。数据如何被格式化的细节丢失在python代码中。为了修改HTML页面,我们的前端工程师还得学会修改python代码。倘若页面十倍或百倍的复杂,这种方式就让人手足无措了。

模板

使用模板来生成HTML页面是一种更好的方式。HTML页面被创作为一个模板,意味着该文件主要还是静态HTML,同时伴有使用特殊符号表示的动态数据片段嵌入其中。上文的极简页面变成模板是这样的:

<p>Welcome, {{user_name}}!</p>
<p>Products:</p>
<ul>
{% for product in product_list %}
    <li>{{ product.name }}:
        {{ product.price|format_price }}</li>
{% endfor %}
</ul>

此时重点就放在HTML文本上了,只有一点逻辑结构嵌入。将这个文本中心的方法与之前的逻辑中心的代码相比。我们之前的程序主要是python代码,有一些HTML代码嵌入python逻辑中。而这里,大部分都是静态HTML标记语言。
模板的多静态风格与大多数编程语言的工作方式恰好相反。比如python的大多数源文件都是可执行代码,如果你需要一个文字式的静态文本,你把它嵌入在一个字符串中:

def hello():
    print("Hello, world!")

hello()

当python读到这个源文件时,它翻译类似于def hello():这样的语句为一个要被执行的指令。而在print("Hello, world!")中双引号表明其中的文本只是字面上的意思。这是大多数编程语言工作的方式:动态为主,同时有少量静态的片段嵌入在指令中。静态部分由引号标记。
模板语言恰好相反:它大多是静态文字文本,同时用特殊的符号表示可执行的动态部分。

<p>Welcome, {{user_name}}!</p>

这里的文本在生成的HTML页面中就以字面出现。直到{{}}表示里面的内容为动态模式,里面的变量将在输出中被替换。
诸如python的"foo = {foo}!".format(foo=17)这样的字符串格式化函数是一种小语言的典型例子,这种语言被用来从字符串字面量和要被插入的数据创建文本。模板拓展了这个想法,包含了条件和循环结构,不同之处只是拓展的程度。
这些文件之所以被称为模板是因为它们被用来产生许多具有相似结构与不同细节的页面。
为了在我们的程序中使用HTML模板, 我们需要一个模板引擎:一个接收参数为一个静态模板(包含结构和页面的静态内容)和一个动态上下文(提供嵌入模板的动态数据)的函数。这个模板引擎结合了模板和上下文来生成一个纯HTML的字符串。模板引擎的任务是翻译模板,用动态数据替换其中的动态片段。
顺便一提,模板引擎并不是针对HTML,它能用来产生任何文本结果。比如,它们也用来生成纯文本电子邮件消息。但是它们通常用于HTML,偶尔也有一些HTML的特定功能,比如escaping(换码),这使得它能过向HTML中插入值而不担心其中是否有HTML中的特殊字符。

支持的语法

模板引擎支持的语法不同。我们的模板语法基于Django,一个流行的Web框架。既然我们用python来实现我们的模板引擎,一些python的概念会出现在我们的语法中。我们已经在本章顶部的极简模板中看到一部分语法,下面是我们将实现的语法的快速摘要。
上下文中的数据使用双大括号插入:

<p>Welcome, {{user_name}}!</p>

当模板被渲染时,模板中的可用的数据由上下文提供。后来更多。
模板引擎通常使用简化的和宽松的语法来提供数据中元素的访问。在python中,这些表述具有不同的效果:

dict["key"]
obj.attr
obj.method()

在我们的模板语法中,所有这些操作都被用点表示:

dict.key
obj.attr
obj.method

圆点将访问对象的属性或者字典的值,并且如果结果值是可调用的,它将被自动调用。这与python代码不同,在python中这些操作具有不同的语法。这导致了简单的模板语法:

<p>The price is: {{product.price}}, with a {{product.discount}}% discount.</p>

你还可以使用被称作过滤器的函数来修改值。过滤器通过一个竖线(管道符)来调用:

<p>Short name: {{story.subject|slugify|lower}}</p>

建立一个有趣的网站通常需要至少一点决策,条件语句要是可用的:

{% if user.is_logged_in %}
    <p>Welcome, {{ user.name }}!</p>
{% endif %}

循环让我们在页面中包含数据集合:

<p>Products:</p>
<ul>
{% for product in product_list %}
    <li>{{ product.name }}: {{ product.price|format_price }}</li>
{% endfor %}
</ul>

正如其他编程语言,条件和循环语句可以嵌套来构复杂的逻辑结构。

最后,让我们可以为模板添加文档,注释出现在大括号和井号之间:

{# This is the best template ever! #}

实现方法

模板引擎具有两个主要的阶段:解析模板,然后渲染模板。
渲染模板具体包括:

  • 管理动态上下文和数据源
  • 执行逻辑元素
  • 实现点访问和过滤器执行

从解析阶段向渲染阶段传递什么东西是问题的关键。解析生产出什么来供渲染?有两个主要的选择,我们叫它们解释和编译,使用了和其他语言实现相关的术语。

在一个解释模型中,解析产生一个数据结构表示模板的结构。渲染阶段遍历那个数据结构,基于找到的指令装配结果文本。一个真实的例子是Django模板引擎使用这种方法。
在一个编译模型中,解析产生某种形式的可直接执行的代码。渲染阶段执行那个代码,产生结果。Jinja2和Mako都是使用编译方法的模板引擎。

我们实现的引擎使用编译方法:我们将模板编译为python代码。执行时,代码将结果组装起来。这里描述的模板引擎一开始是作为coverage.py的一部分写的,来生成HTML报告。在coverage.py中,只有很少的模板,它们被反复利用产生很多文件。总体而言,如果模板被编译为python代码,程序运行速度更快,因为即使编译过程比较复杂,它也只需要运行一次,而被编译的代码执行了很多次,要比解释一个数据结构很多次快很多。

将模板编译为python代码有点复杂,但是没有你想的那么糟糕。此外,编写能够写代码的程序比编写程序本身有趣多了!我们的模板编译器是一个代码生成的通用技术的小例子。代码生成技术构成许多强大而灵活的工具的基础,包括编程语言编译器。代码生成可以变得很复杂,但它是一个很值得拥有的有用的技术。

如果模板每次只会被使用很少次,这样的模板应用可能倾向于解释方法。编译模板为python代码的代价从长远看有些打了,整体看来,一个更简单的解释过程可能会更好。

编译到Python

在得到模板引擎的代码之前,我们先看看要它生成的代码。解析阶段将一个模板转换为一个Python函数。再次使用我们的小模板:

<p>Welcome, {{user_name}}!</p>
<p>Products:</p>
<ul>
{% for product in product_list %}
    <li>{{ product.name }}:
        {{ product.price|format_price }}</li>
{% endfor %}
</ul>

我们的引擎将编译这个模板为python代码。python代码的结果看上去不同寻常,因为我们选择了一些快捷方式来产生轻量级和更快的代码。下面的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([
        '<p>Welcome, ',
        to_str(c_user_name),
        '!</p>\n<p>Products:</p>\n<ul>\n'
    ])
    for c_product in c_product_list:
        extend_result([
            '\n    <li>',
            to_str(do_dots(c_product, 'name')),
            ':\n        ',
            to_str(c_format_price(do_dots(c_product, 'price'))),
            '</li>\n'
        ])
    append_result('\n</ul>\n')
    return ''.join(result)

每个模板都被转换为一个render_function函数,其接受一个叫做context的数据字典。函数体先解包上下文字典中的数据到本地变量,因为对于数据的重复使用这样会快些。所有的上下文数据以加上c_前缀的形式变为本地变量这样我们可以自由使用本地变量名而不用担心命名冲突。

模板的结果将是一个字符串。从部件构建一个字符串最快的方式就是创建一个字符串列表,然后将它们组合在一起。result就是一个字符串列表。因为我们将添加字符串到这个列表中,我们捕捉了它的appendextend方法赋给本地变量result_appendresult_extend。最后一个创建的本地变量是一个内置方法str的速记--to_str

这些形式的快捷键并不寻常。让我们看得更仔细些:在python中,一个被对象调用的方法如result.append("hello")分两步执行。首先,append属性从result对象中获取,然后取得的值被作为函数调用,传递参数“hello”给它。尽管我们习惯于看到这些步骤被一起执行,实际上它们是分开的。如果你储存了第一步的结果,那么你将在储存的结果上执行第二步。所以下面这两个代码片段做的是同样的事:

# The way we're used to seeing it:
result.append("hello")

# But this works the same:
append_result = result.append
append_result("hello")

在模板引擎代码中我们用这种分离的方式是的我们不论做多少次第二步,只用做一次第一步。这节省了我们少量的时间,因为避免了再花时间去查找对象的append属性。

这是一个微型优化的例子:一个不同寻常的编码技术使我们获得速度上的微小改进。微型优化可能会使代码变得可读性差或更令人困惑,所以只对于那些被证明是性能瓶颈的代码使用才是合理的。开发者对于怎样的微型优化是合理的存在分歧,而一些新手会过度使用它。这里的优化只在时间测试表明它们提升了性能的情况下被加上,即使是一点点的提升。微优化具有启发性,因为它们使用了python的某些奇异的方面,但是别在你自己的代码中过度使用它。
str的快捷方式同样是一个微优化。在python中变量可以是函数本地的或者模块全局的或者是python内置的。查找一个本地变量名的速度要比查找一个全局或内置的名称快。我们习惯于str是一个总是可获得的内置函数,但是python仍然不得不在每次使用它时查找变量名。将它放在一个本地变量中又为我们节省了一小块的时间,因为本地的要比内建的快。
一旦这些快捷键被定义,下面就是考虑从我们的特定模板中生成的python代码。字符串将被使用append_result或者extend_result快捷键添加到result列表,选择前一个还是后一个取决于我们只有一个字符串要添加还是多个。模板中的文本变成了一个简单的字符串。
同时具有appendextend方法增加了复杂性,但请记住我们的目的是模板的最快执行。对一个项目使用extend意味着要创建该项目的新列表这样我们才能将它传递给extend
{{...}}中的表达式将被计算,转换为字符串,并被添加到result。表达式中的点将被传入渲染函数的do_dots函数处理,以为加点的表达式的意义取决于context中的数据形式:它可能是属性访问、子项目获取或者是一个调用。
{% if ... %}{% for ... %}的逻辑结构都转换为python的条件语句和循环。在{% if/for ... %}标签中的表达式将会变成if/for语句中的表达式,然后直到{% end... %}标签之前的内容都会变成语句的主体。

下一步就是实现了,请看下篇

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,294评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,493评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,790评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,595评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,718评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,906评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,053评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,797评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,250评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,570评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,711评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,388评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,018评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,796评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,023评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,461评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,595评论 2 350

推荐阅读更多精彩内容