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,如果postsNone,实际是一个新建的空集合[]。否则,使用传入的值为文章赋值。

    另外,还定义了一个方法,基于标签为博文创建索引。在返回的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"),
        )
    )

    我们已经将Blogpost序列化成了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()调用,来确保类似titleunderline这样的属性可作为关键字参数。

    {%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不可以迭代,这使得渲染博客中所有文章的这项任务变得有一些复杂。