内容


使用 Python 和 Cheetah 构建和扩充模板

使用 Python 的功能强大的模板引擎生成 HTML、XML、纯文本和更多类型的文本

Comments

一个过于臃肿的模板系统

使用 SQLObject 连接数据库与 Python”中提到各种用于 Python 的开源对象关联式映射库。Python 编程人员喜欢按照自己的方式办事,这带来了许多附属成果。不过,所有这些努力常常汇集成一个对所有人都非常有益的包。

同样的模式对模板系统却已经过时:表示静态文本的方式将像窗体一样被扩充,这样可以随后插入动态元素。官方的 Python Wiki 链接了近 20 个模板系统,这些只是一些主要的模板系统。还有更多的模板系统,随 Python 一起打包的是一些基本的模板系统,这些模板系统将在简单的情形下工作。

本文将描述模板系统可以解决的问题。还将描述 Cheetah,它是已经设计出来的最好的 Python 模板系统。本文假定您具备 Python 方面的基础知识,但不具备模板系统及其用处方面的知识。

基本的模板概念

假定您正在为一家在线商店编写一个 Web 应用程序。您需要一些类来表示这家商店和购买过程的某些方面:库存物品、客户和客户所下的定单,等等。这些类的实例可能对应于数据库中的行,并且它们被用来表示商店和客户的状态。为了使您和您的客户使用该应用程序,需要使用这些对象生成可读的 HTML 页面和电子邮件消息,如下面电子邮件所示:

清单 1. Hello 代码
Hello, Leonard.
Your order (#98765) has shipped:
 Widget, green: 50 unit(s)
 Widget, blue: 1 unit(s)
Your tracking number is 1234567890AB.

这封电子邮件的正文由一个静态部分和一个动态部分组成。静态部分是消息的抽象形式,它总是相同的。可以用如下所示伪代码描述它:

清单 2. 模板的抽象形式
Hello, [customer's first name].
Your order (#[order's ID]) has shipped:
 [list items in the order, with quantity, appending "unit(s)" to each]
Your tracking number is [order's tracking number].

电子邮件正文的动态部分是与来自某个特定客户的特定定单有关的所有信息。这部分是由应用程序对象以及与它们有关的数据成员表示的,比如,定单、物品列表和定单的数量。

接下来这一节将使用虚构的 UserOrder 对象来模拟模板的动态部分。在实际应用程序中,这些对象可能是从一个数据库中获得的。

模板系统允许您将文本的静态部分表示为一个模板定义,并与动态部分分开存储和管理。静态部分可以与不同的动态值组合在一起,生成定制文本。

下面几节将解释 Python 内置工具的缺点,并介绍如何使用 Cheetah 作为替代,以及如何以 Cheetah 模板的形式实现这个电子邮件消息。

为什么要使用模板系统?

如果没有模板系统,那么就要使用 Python 代码生成类似示例电子邮件消息的文本片段。还要编写附加到字符串列表的逻辑或为类文件对象编写的逻辑。例如,以下代码可以生成上面所述的电子邮件:

清单 3. 生成电子邮件
from DummyObjects import dummyUser, dummyOrder
l = []
l.append('Hello, ')
l.append(dummyUser.firstName)
l.append('.\n\nYour order (#')
l.append(str(dummyOrder.id))
l.append(') has shipped:\n')
for purchased, quantity in dummyOrder.purchased.items():
     l.append(' ')
     l.append(purchased.name)
     l.append(': ')
     l.append(str(quantity))
     l.append(' unit(s)\n')
l.append('\nYour tracking number is ')
l.append(dummyOrder.trackingNumber)
l.append('.')
print ''.join(l)

不幸的是,这个代码不像它的输出那样好。通过查看此代码,难以看出电子邮件消息的结构。许多代码是重复的(例如,有许多对列表的 append() 方法的调用),这为错误创造了空间。最后,团队的 UI 设计人员可能宁愿编辑类似上面描述的抽象模板那样的东西,也不愿意编辑这种 Python 代码。由于上述这些原因,开发人员转向模板系统。

有 Python 的内置模板系统就足够了吗?

Python 提供了少许内置模板系统,它们在简单情况下工作得很好。很长时间以来,Python 一直拥有一些简单的模板系统,它们有一些易于让人理解的格式,这些格式令人回想起 C 的 printf() 字符串格式:

清单 4. Python 的内置模板系统
from DummyObjects import dummyUser, dummyOrder
print 'Hello, %s.\n\nYour order (#%d) has shipped:' % (dummyUser.firstName, 
                                                       dummyOrder.id)
print 'Hello, %(firstName)s.\n\nYour order (#%(orderID)d) has shipped:' % \
{'firstName' : dummyUser.firstName, 'orderID' : dummyOrder.id}

Python V2.4 引入了一个具有看起来更现代的格式的模板系统。变量名称是通过添加美元符号($)作为前缀来指定的;这类似于 Perl、PHP、大多数 shell 语言和 Cheetah:

清单 5. Python V2.4 的内置模板系统
from string import Template
from DummyObjects import dummyUser, dummyOrder
t = Template('Hello, $firstName.\n\nYour order (#$orderID) has shipped:')
t.substitute({'firstName' : dummyUser.firstName, 'orderID' : dummyOrder.id})

这些模板系统都有两个重要的缺点。

  1. 它们的模板定义都不能调用任何方法,或者访问 dummyUserdummyOrder 对象的任何成员。不能将 dummyUser.firstName 放入模板定义中,必须将它放入应用于模板定义的映射中。所有将插入静态模板定义中的动态信息都必须首先拆分为基本的 Python 数据类型。
  2. 这些模板系统没有流控制 —— 没有循环或条件。以前的那些例子恰好在必须按顺序在项上进行迭代之前停止执行,并且有一个好的理由:它们使用的模板系统无法在模板定义内部进行这种迭代。您需要编写 Python 代码在这些项上按顺序进行迭代,将多个字符串连接在一起(可能使用中间模板),并以称为 itemsOrdered 的单个字符串的形式向模板系统提供最终结果。循环本身是电子邮件主体的静态部分的一部分 —— 无论处理什么样的用户和定单,其工作方式是相同的 —— 但无法将它应用到静态模板定义中。

多数可用于 Python 的附加模板系统试图弥补这两个缺点。其中最好的替代是 Cheetah。

Cheetah 简介

Cheetah 有一个很长的家谱。它从称为 Velocity 的 Java™ 模板系统那里获得了灵感,Velocity 是 Webmacro 模板系统的一个改进版本,试图在 JavaServer Pages 上进行改进。Cheetah 提供了一门简单语言,用来定义提供基本流控制和对象访问构造的模板。它借用了 Velocity 的基本模板语法,但添加了一些特性,为 Cheetah 模板提供对 Python 的便利构造的访问。

以下是一些 Cheetah 代码,这些代码解释了模板定义的“简单”部分 —— 没有流控制的那一部分,Python 的内置模板系统可以处理它:

清单 6. 第一个 Cheetah 示例
from Cheetah.Template import Template
from DummyObjects import dummyUser, dummyOrder
definition = """Hello, $user.firstName.
Your order (#$order.id) has shipped:"""
print Template(definition, searchList=[{'user' : dummyUser,
                                        'order' : dummyOrder}])

definition 字符串包含模板定义(电子邮件的静态部分),它可以对外部变量(动态部分)进行引用。Template 构造函数在这里用来将模板定义绑定到名称空间searchList:查找对象的方式对应于定义中使用的变量。例如,模板定义中的 $user 在这里映射到 dummyUser 变量。您还可以提前运行 Template 构造函数,并在准备使用特定对象解释模板的时候设置其 searchList 成员。

您应该已经看到 Cheetah 胜过 Python 的内置模板系统的一些优点。消息的动态部分(dummyUserdummyOrder 对象)只是模板定义中不作考虑的事项。其他所有事项,包括要访问的对象成员,都不能在消息之间进行更改,因此都纳入到模板定义中。

假设您需要更改电子邮件模板,以便打印用户的全名,而不是用户的名字。假定 dummyUser 对象已经提供了该信息(例如,带有一个 getFullName() 方法或一个 fullname 成员),您可以通过更改模板定义单独进行此更改。而在使用内置 Python 模板系统时,则必须更改 Python 代码。

将模板定义编译成 Python 类

在将模板定义转变成一个 Template 对象时,会发生什么?Cheetah 生成了一个定制 Python 类,该类实现代码来合并模板定义和动态数据。通过将模板定义保存为一个文件并在其上运行 cheetah compile 命令,您可以自行观察这一点。以下是 Greeting.tmpl,它是包含前面所使用的相同 Cheetah 模板的一个模板文件:

Hello, $user.firstName.
Your order (#$order.id) has shipped:

在这个文件上运行 cheetah compile Greeting.tmpl 将生成一个称为 Greeting.py 的模块。这个类包含一个称为 Greeting 的类,后者实现的代码非常类似于文章开始部分手工编写的代码:

手工代码Cheetah 生成的代码
l.append('Hello, ')
l.append(
   dummyUser.firstName)



l.append('.\n\nYour
   order (#')
l.append(str(
   dummyOrder.id))


l.append(') has
   shipped:\n')
write('Hello, ')
write(filter(VFFSL(SL,
   "user.firstName",
   True), rawExpr=
   '$user.firstName'))
write('.\n\nYour
   order (#')
write(filter(VFFSL(SL,
   "order.id",True),
   rawExpr=
   '$order.id'))
write(') has
   shipped:')

然后,您可以在 Python 中使用生成的 Greeting 类,就像您已经使用 Greeting.tmpl 的内容定义了一个通用 Template 一样:

from Greeting import Greeting
print Greeting(searchList=[{'user' : dummyUser, 'order' : dummyOrder}])

因为 Cheetah 可以将模板文件编译成 Python 代码,所以您可以预先解析所有模板,并在使用动态数据扩充代码时,从已编译的代码中获得好处。

流控制:#for 指令

在生成示例模板的第一部分时,Cheetah 比 Python 的内置模板系统更好用。它还处理模板的其余部分,而这些部分是内置系统根本无法处理的。除了变量引用之外,Cheetah 模板定义还包含针对 Cheetah 解释器的指令,其中包括设置循环的 #for 指令:

清单 7. 使用 #for 指令在列表上进行迭代
definition = """Hello, $user.firstName.
Your order (#$order.id) has shipped:
#for $purchased, $quantity in $order.purchased.items():
 $purchased.name: $quantity unit(s)
#end for
Your tracking number is $order.trackingNumber."""
print Template(definition, searchList=[{'user' : dummyUser,
			                'order' : dummyOrder}])

此代码与以前的代码完全相同,但模板定义是不同的。#for 指令启动了一个循环,而 #end for 指令结束了这个循环。因为可以用 Cheetah 生成空白空间很重要的文本(比如电子邮件消息),所以 Cheetah 无法像 Python 所做的那样,将空白空间用作流控制机制,因此它要使用 #end 指令。

#for 迭代的作用类似于使用 Python 的 for 关键字的 Python 迭代。此迭代的作用完全类似于上面所示的手工编写的 Python 迭代:

for purchased, quantity in order.items():
     l.append(purchased.name)
     ...

在手工编写的 Python 代码中,必须将每个输出项手工添加到输出字符串的列表 l 中。Cheetah 使得下列事情变得更轻松:它评估 #for 循环中的代码,并自动将每个迭代的输出附加到模板的输出中。

Cheetah 还提供了一个 #while/#end while 指令,该指令等同于 Python 的 while 构造。

流控制:#if 指令

您已经创建了一个重新生成上述电子邮件的 Cheetah 模板。现在,让我们对它稍微做一下改进。这封电子邮件说您订购了“1 单位”的蓝色装饰品。如果您只订购一件东西或者订购“x 单位”的其他东西,那么更改模板使其说出“1 单位”应该不是很困难。Cheetah 提供了一个 #if 指令,该指令允许您设置 if-then-else 条件。以下是一个 Cheetah 的模板,它试图正确处理复数。Python 代码总是相同的,因此,以下代码只显示了新的模板定义:

清单 8. 使用 #if 指令处理复数
Hello, $user.firstName.
Your order (#$order.id) has shipped:
#for $purchased, $quantity in $order.purchased.items():
 $purchased.name: $quantity unit
#if $quantity != 1
s
#end if
#end for

使用这个模板定义的惟一问题是,Cheetah 在单位 中的一个单独行上打印 s

 Widget, green: 50 unit
s
 Widget, blue: 1 unit

可以用两种方法避免这种尴尬的现象:禁止这种令人讨厌的新行,或者提前设置适当的字符串作为变量。

使用 #slurp 指令禁止出现这种新行

Cheetah 的指令 #slurp 告诉 Cheetah 不要在特殊行的结尾处打印新行:

清单 9. 禁止在行的结尾处出现新行
#for $purchased, $quantity in $order.purchased.items():
 $purchased.name: $quantity unit#slurp
#if $quantity != 1
s
#end if
#end for

此代码提供了您想要的输出:

 Widget, green: 50 units
 Widget, blue: 1 unit

使用 #set 指令设置变量

如果 #slurp 指令对您来说不好用,那么可以使用另一种方法。可以使用 #set 指令在 Cheetah 模板的作用域内创建一个临时变量:

清单 10. 使用 #set 创建一个临时变量
#for $purchased, $quantity in $order.purchased.items():
 #if $quantity == 1
  #set $units = 'unit'
 #else
  #set $units = 'units'
 #end if
 $purchased.name: $quantity $units
#end for

在这种情况下,#set 允许您将条件从文本生成中移出,移动到定义变量的代码中。#set 通常是一个有用的指令。您还可以使用它来创建中间值,或者用它来避免多次调用某一昂贵的方法。

生成其他类型的文件

为了简便起见,迄今为止的所有示例生成的都是纯文本的电子邮件,但您不必拘泥于此。Cheetah 最初被设计为生成 HTML 文本,但您可以用它生成任何基于文本的格式:XML、SQL,甚至是 Python 或其他编程语言代码。

以下是一个 Cheetah 模板,它提供了一个呈现定单状态页的 HTML。可以将这个 HTML 转变成称为 OrderStatus.tmpl 的文件。注意,它与相同信息的电子邮件转换的基本相似性:

清单 11. 呈现定单状态信息的 HTML
<html>
<head><title>Status for order #$order.id</title></head>
<body>
<p>[You are logged in as $user.getFullName().]</p>
<p>
#if ($order.hasShipped())
 Your order has shipped. Your tracking number is $order.trackingNumber.
#else
 Your order has not yet shipped.
#end if
</p>
<p>Order #$order.id contains the following items:</p>
<ul>
#for $purchased, $quantity in $order.purchased.items():
 <li>$purchased.name: $quantity unit#slurp
#if ($quantity != 1)
s
#end if
</li>
#end for
</ul>
<hr />
Served by Online Store v1.0
</body>
</html>

组合模板

前面定义的 HTML 定单状态页包含某些元素,这些元素对所有在线商店的 Web 应用程序都(应该)很常见:一个带有特定于页面的小标题的 HTML 标题、一个位于页面顶部告诉您您已经进入系统的通知以及一个 HTML 脚注。可能还应该将这些常见页面元素引入某个单独的主模板中。这样,OrderStatus.tmpl 中的模板定义将只包含专门用来显示定单状态的代码,而其他所有模板定义可以使用主模板中创建的代码,不必每次都定义相同的代码。

大多数模板系统(包括 Cheetah 及其 #include 指令)都允许您从另一个模板调用一个模板。您可以使用这项功能,将常见的模板内容移入(例如)Header.tmpl 和 Footer.tmpl 文件中。不过,Cheetah 还允许使用一种更好的方法来解决这个问题:通过使模板进行细分成为可能。

以下是 Skeleton.tmpl,是定义了一个 THML 页的主干的模板定义,但它很明显地留下了两个项没有进行绑定:$title$body

清单 12. Skeleton.tmpl 的模板定义
<html>
<head><title>$title</title></head>
<body>
<p>[You are logged in as $user.getFullName().]</p>
$body
<hr />
Served by Online Store v1.0.
</body>
</html>

回忆一下前面的内容,我们通过在 Skeleton.tmpl 文件上运行 cheetah compile 命令生成了 Skeleton.py,这是一个包含称作 Skeleton 的 Python 类的包,这个包的作用类似于 Skeleton.tmpl。一旦文件准备就绪,就可以编写定单状态模板了。OrderStatusII.tmpl 可以导入生成的 Skeleton 类并对其进行细分。您还可以定义 $title$body 的值,以及主模板中引用的变量:

清单 13. 细分 Skeleton.tmpl
#from Skeleton import Skeleton
#extends Skeleton
#def title
Status for order #$order.id
#end def
#def body
<p>
#if ($order.hasShipped())
 Your order has shipped. Your tracking number is $order.trackingNumber.
#else
 Your order has not yet shipped.
#end if
</p>
<p>Order #$order.id contains the following items:</p>
<ul>
#for $purchased, $quantity in $order.purchased.items():
 <li>$purchased.name: $quantity unit#slurp
#if ($quantity != 1)
s
#end if
 </li>
#end for
</ul>
#end def

OrderStatusII.tmpl 使用 #extends 指令来声明其模板是 Skeleton.tmpl 中定义的模板的一个特例。然后,它使用 #extends 指令定义称为 titlebody 的函数。这些函数对应于主干模板中使用的 $title$body 变量。它们不会在这里 #set(设置)$title$body 的值: #set 指令设置 Python 变量的值,但 titlebody 则需要对应于 Python 函数。

在 Python 中使用 def 关键字时,还可以使用 Cheetah 的 #def 指令在模板内部定义函数。然后,可以多次调用该模板,从而避免复制和粘贴代码。

结束语

Cheetah 提供了比这里描述的更多的特性。例如,您可以设置一个筛选器,以某种特定的方式修改所有变量引用的输出。还可以使用 #import 指令将任意 Python 模块导入 Cheetah 模板中,并调用它们的函数。实际上,几乎在 Python 中可以所做的所有事情都能在 Cheetah 中实现。

不过,建议您使所有事情尽量简单。请记住,模板系统的目标是将文档的动态部分与其静态部分分离。从将应用程序代码放入 Cheetah 模板开始,您会发现自己遇到同样头疼的问题 —— 迫使编程人员和 UI 设计人员首先选择模板系统。Cheetah 哲学的一个方面是:“Python 适用于后端,Cheetah 适用于前端。”根据这一经验法则,您应该畅通无阻地获取模板系统的好处。


下载资源


相关主题


评论

添加或订阅评论,请先登录注册

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Open source
ArticleID=93499
ArticleTitle=使用 Python 和 Cheetah 构建和扩充模板
publish-date=09052005