9.3 定义用于持久化的类
在开始进行持久化之前,需要先获得要保存的对象。关于持久化的设计有几个要点需要考虑,将以一个简单的类定义为起始。我们将看一个简单的博客和上面所发布的文章,以下是一个Post类的定义。
import datetime
class Post:
def init( self, date, title, rst_text, tags ):
self.date= date
self.title= title
self.rst_text= rst_text
self.tags= tags
def as_dict( self ):
return dict(
date= str(self.date),
title= self.title,
underline= "-"*len(self.title),
rst_text= self.rst_text,
tag_text= " ".join(self.tags),
)
每篇博客的文章属性中包含了这些实例变量:日期、标题、一些文字和一些标签。属性名称中已经暗示了文字需要使用RST标记,尽管这在很大程度上与其他的数据模型无关。
为了对简单的模板替换进行支持,as_dict()方法会返回一个字典,其中的每个值都被转换为字符串格式。接下来会介绍如何使用string.Template进行模板处理。
补充一点说明,我们已经加入了一些值用来辅助创建RST的输出结果。tag_text属性是一个使用纯文本进行标记的元组值。underline属性生成了一个以下划线为起始的字符串,它的长度与标题字符串是相匹配的,这使得RST格式化的操作很方便。我们也会创建一篇包含了多篇文章的博文。通过在标题上附加属性来实现一个集合,而不是简单地使用一个列表。在进行集合设计时有3种选择:封装、扩展或新建。可以结合这一点来进行设计,为了减少一些困惑:如果打算持久化就不要扩展list。
| 扩展一个可迭代的对象可能会造成困惑 当扩展一个序列时,我们可能影响了其内置的序列化算法。当通过在子类加入功能来扩展一个序列时,其内置的算法可能并不会调用我们的实现。比起扩展一个序列,封装通常更妥当。 |
这强制我们必须使用封装或新建的方式。如果只需要一个简单的序列,为什么要新建呢?封装才是我们所推荐的设计策略。这里是一个微博文章的集合,封装了一个集合,因为对集合扩展的方式有些不够稳定。
from collections import defaultdict
class Blog:
def init( self, title, posts=None ):
self.title= title
self.entries= posts if posts is not None else []
def append( self, post ):
self.entries.append(post)
def by_tag(self):
tag_index= defaultdict(list)
for post in self.entries:
for tag in post.tags:
tag_index[tag].append( post.as_dict() )
return tag_index
def as_dict( self ):
return dict(
title= self.title,
underline= "="*len(self.title),
entries= [p.as_dict() for p in self.entries],
)
为了更完整地完成集合的封装,也使用了一个属性作为微博的标题。初始化过程使用了常用的技术来将默认值创建为不可变对象。posts的默认值为None,如果posts是None,实际是一个新建的空集合[]。否则,使用传入的值为文章赋值。
另外,还定义了一个方法,基于标签为博文创建索引。在返回的defaultdict结果中,每个键实际就是标签的文本,每个值是每个标签所对应的文章列表。
为了简化string.Template的使用,我们添加了另一个as_dict方法,用于将博客转换为一个简单的由字符串所组成的字典或字典的集合。这个思路就是基于内置类型来生成字符串的表达形式。接下来将介绍模板的处理过程,以下是一些示例数据。
travel = Blog( "Travel" )
travel.append(
Post( date=datetime.datetime(2013,11,14,17,25),
title="Hard Aground",
rst_text="""Some embarrassing revelation.
Including ☹and ⎕""",
tags=("#RedRanger", "#Whitby42", "#ICW"),
)
)
travel.append(
Post( date=datetime.datetime(2013,11,18,15,30),
title="Anchor Follies",
rst_text="""Some witty epigram. Including < & >
characters.""",,
tags=("#RedRanger", "#Whitby42", "#Mistakes"),
)
)
我们已经将Blog和post序列化成了Python代码。这种表示博客的方式并不是没有优势。对于一些使用场景,Python代码恰恰是表达对象的最佳方式。在第13章“配置文件和持久化”中,我们将更深入地介绍如何使用Python对数据进行编码。
渲染博客与文章列表
在实现的过程中,这里采用了将博客渲染为RST的方式。从输出文件来看,docutils中的rst2html.py工具可用于将RST结果转换为最终的HTML文件。这避免了我们做一些额外的有关HTML和CSS的工作。而且,在第18章“质量和文档”中,我们将使用RST来编写文档。有关docutils的更多内容,可查看前言部分的一些内容。
可以使用string.Template类来完成这项工作。然而,它显得不够轻巧而且很复杂。有很多插件模板工具可以实现更复杂的处理过程,包括模板中的循环和条件语句处理。这里是一个列表:https://wiki.python.org/moin/Templating。我们将介绍一个使用Jinja2模板工具的示例,详细介绍参见https://pypi.python.org/pypi/Jinja2。以下是一个基于模板使用脚本来实现RST数据的渲染过程。
from jinja2 import Template
blogtemplate= Template( """
{{title}}
{{underline}}
{% for e in entries %}
{{e.title}}
{{e.underline}}
{{e.rst_text}}
:date: {{e.date}}
:tags: {{e.tag_text}}
{% endfor %}
Tag Index
=========
{% for t in tags %}
* {{t}}
{% for post in tags[t] %}
- '{{post.title}}'
{% endfor %}
{% endfor %}
""")
print( blog_template.render( tags=travel.by_tag(), **travel.as_dict()
) )
{{title}}和{{underline}}元素(和所有类似的元素),演示了它们的值是如何被替换为模板中的文字的。render()方法被**travel.as_dict()调用,来确保类似title和underline这样的属性可作为关键字参数。
{%for%}和{%endfor%}结构演示了Jinja如何对Blog中的所有Post集合进行迭代。在循环体中,变量e是基于每个Post所创建的字典。我们为每篇文章从字典中选择了不同的键:{{e.title}},{{e.rst_text}},等等。
我们也为Blog做了tags集合的迭代操作。它是一个字典,键为标签,值为标签对应的文章列表。循环将访问每个键,然后赋值给 t。在循环体中会迭代字典中所存放的每一篇文章,即tags[t]。
'{{post.title}}'结构是一个RST标记,用于生成指向文档中每个标题所对应的节。这类简单的标记是RST的优势之一。在索引范围内,我们将博客标题作为节和链接。这意味着标题必须是唯一的,否则将得到RST渲染错误。
由于这个模板会对指定博客进行迭代,因此它将一次性渲染所有的文章。而Python中内置的string.Templete不可以迭代,这使得渲染博客中所有文章的这项任务变得有一些复杂。
