持久性是指保持对象,甚至在多次执行同一程序之间也保持对象。本文将介绍 Python 对象的各种持久性机制,包括将对象数据存储在某种格式的文本文件中、关系数据库、Python 的 pickle 以及其他机制。这些存储机制都有一个共同点:存储的数据是独立于对这些数据进行操作的对象和程序。这样做的好处是,数据可以作为共享的资源,供其它应用程序使用。缺点是,用这种方式,可以允许其它程序访问对象的数据,这违背了面向对象的封装性原则 — 即对象的数据只能通过这个对象自身的公共(public)接口来访问。

对于某些应用程序,关系数据库方法可能不是很理想。尤其是,关系数据库不理解对象。相反,关系数据库会强行使用自己的类型系统和关系数据模型(表),每张表包含一组元组(行),每行包含具有固定数目的静态类型字段(列)。如果应用程序的对象模型不能够方便地转换到关系模型,那么在将对象映射到元组以及将元组映射回对象方面,会碰到一定难度。这种困难常被称为阻碍性不匹配(impedence-mismatch)问题。

如果希望透明地存储 Python 对象,而不丢失其身份和类型等信息,则需要某种形式的对象序列化:它是一个将任意复杂的对象转成对象的文本或二进制表示的过程。同样,必须能够将对象经过序列化后的形式恢复到原有的对象。在 Python 中,这种序列化过程称为 pickle,可以将对象 pickle 成字符串、磁盘上的文件或者任何类似于文件的对象,也可以将这些字符串、文件或任何类似于文件的对象 unpickle 成原来的对象。我们将在本文后面详细讨论 pickle。

如果您喜欢将任何事物都保存成对象,而且希望避免将对象转换成某种基于非对象存储的开销,那么 pickle 文件可以提供这些好处。然而,有时可能需要比这种简单的 pickle 文件更健壮以及更具有可伸缩性的解决方案。例如,只用 pickle 不能解决命名和查找 pickle 文件这样的问题,另外,它也不能支持并发地访问持久性对象。如果需要这些方面的功能,则要求助于类似于 ZODB(针对 Python 的 Z 对象数据库)这类数据库。

ZODB 是一个健壮的、多用户的和面向对象的数据库系统,它能够存储和管理任意复杂的 Python 对象,并支持事务操作和并发控制。要有效地使用 ZODB,必须充分了解 pickle。令人足够感兴趣的是,甚至 ZODB 也依靠 Python 的本机序列化能力。

另一种令人感兴趣的解决持久性问题的方法是 Prevayler,它最初是用 Java 实现的。最近,一群 Python 程序员将 Prevayler 移植到了 Python 上,另起名为 PyPerSyst,由 SourceForge 托管。Prevayler/PyPerSyst 概念也是建立在 Java 和 Python 语言的本机序列化能力之上。

PyPerSyst 将整个对象系统保存在内存中,并通过不时地将系统快照 pickle 到磁盘以及维护一个命令日志来提供灾难恢复。尽管使用 PyPerSyst 的应用程序受到可用内存的限制,但好处是本机对象系统可以完全装入到内存中,因而速度极快。而且实现起来要比像 ZODB 这样的大型数据库简单得多,ZODB 允许对象的数目比同时在内存中所保持的对象要多。

既然我们已经简要讨论了存储持久对象的各种方法,那么现在该详细探讨 pickle 过程了。虽然我们主要感兴趣的是探索以各种方式来保存 Python 对象,而不必将其转换成某种其它格式,但我们仍然还有一些需要关注的地方,譬如:如何有效地 pickle 和 unpickle 简单对象以及复杂对象,包括定制类的实例;如何维护对象的引用,包括循环引用和递归引用;以及如何处理类定义发生的变化,从而使用以前经过 pickle 的实例时不会发生问题。我们将在随后关于 Python 的 pickle 能力探讨中涉及所有这些问题。

一些经过 pickle 的 Python

pickle 模块及其同类模块 cPickle 向 Python 提供了 pickle 支持。后者是用 C 编码的,它具有更好的性能,对于大多数应用程序,推荐使用该模块。我们将继续讨论 pickle ,但本文的示例实际是利用了 cPickle 。由于其中大多数示例要用 Python shell 来显示,所以先展示一下如何导入 cPickle ,并可以作为 pickle 来引用它:

```python

import cPickle as pickle

```

现在已经导入了该模块,接下来让我们看一下 pickle 接口。 pickle 模块提供了以下函数对:

- `dumps(object)`:返回一个字符串,它包含一个 pickle 格式的对象;

- `loads(string)`:返回包含在 pickle 字符串中的对象;

- `dump(object, file)`:将对象写到文件,这个文件可以是实际的物理文件,但也可以是任何类似于文件的对象,这个对象具有 write() 方法,可以接受单个的字符串参数;

- `load(file)`:返回包含在 pickle 文件中的对象。

缺省情况下, `dumps()` 和 `dump()` 使用可打印的 ASCII 表示来创建 pickle。两者都有一个 final 参数(可选),如果为 True ,则该参数指定用更快以及更小的二进制表示来创建 pickle。`loads()` 和 `load()` 函数自动检测 pickle 是二进制格式还是文本格式。

清单 1 显示了一个交互式会话,这里使用了刚才所描述的 `dumps()` 和 `loads()` 函数:

清单 1. `dumps()` 和 `loads()` 的演示:

注:pickle 格式非常简单,这里就不详细解释了。实际上,在 pickle 模块中记录了所有使用的约定。我们还需要指出的是,在我们的示例中使用的都是简单对象,因此使用二进制 pickle 格式在节省空间方面的效果并不明显。然而,在实际使用复杂对象的系统中,你会发现,使用二进制格式可以在大小和速度方面带来显著的改进。

接下来,我们来看一些示例,这些示例用到了 dump() 和 load() 函数,它们可以处理文件和类似文件的对象。这些函数的操作与我们刚才看到的 dumps() 和 loads() 非常相似,唯一的区别在于它们还具有另一种能力——dump() 函数可以将多个对象一个接一个地转储到同一个文件中。随后调用 load() 以相同的顺序检索这些对象。清单 2 展示了这种能力的实际应用:

清单 2. dump() 和 load() 示例:

Pickle 的强大之处

至此,我们已经讲解了关于 pickle 的基本知识。在本节中,我们将讨论一些高级问题,当您开始处理 pickle 中的复杂对象时,可能会遇到这些问题,其中包括定制类的实例。幸运的是,Python 可以很容易地处理这种情况。

可移植性

从空间和时间的角度来看,Pickle 是可移植的。换句话说,pickle 文件格式独立于机器的体系结构,这意味着,例如,可以在 Linux 下创建一个 pickle 文件,然后将其发送到在 Windows 或 Mac OS 下运行的 Python 程序。此外,当升级到更新版本的 Python 时,也不需要担心可能废弃的已有 pickle。Python 开发人员已经保证 pickle 格式将向后兼容各个 Python 版本。实际上,在 pickle 模块中提供了有关当前和支持的格式的详细信息.

清单 3. 可检索的格式:

多个引用指向同一个对象

在 Python 中,变量是对对象的引用。同时,也可以用多个变量引用同一个对象。Python 在维护这种行为方面没有任何困难,如清单 4 所示:

清单 4. 对象引用的维护:

循环引用和递归引用

可以将前面演示过的对象引用支持扩展到循环引用(两个对象各自包含对方的引用)和递归引用(一个对象包含对其自身的引用)。以下两个清单重点展示了这种能力。首先看一下递归引用:

清单 5. 递归引用:

现在,让我们看一个循环引用的示例:

清单 6. 循环引用:

注意,如果分别 pickle 每个对象,而不是在一个元组中一起 pickle 所有对象,会得到略微不同(但很重要)的结果,如清单 7 所示。

清单 7. 分别 pickle vs. 在一个元组中一起 pickle:

相等,但并不总是相同

正如在上一个示例所暗示的,只有在这些对象引用内存中同一个对象时,它们才是相同的。在 pickle 情形中,每个对象被恢复到一个与原来对象相等的对象,但不是同一个对象。换句话说,每个 pickle 都是原来对象的一个副本:

清单 8. 作为原来对象副本的被恢复的对象:

同时,我们看到 Python 能够维护对象之间的引用,这些对象是作为一个单元进行 pickle 的。然而,我们还看到分别调用 dump() 会使 Python 无法维护对在该单元外部进行 pickle 的对象的引用。相反,Python 复制了被引用对象,并将副本和被 pickle 的对象存储在一起。对于 pickle 和恢复单个对象层次结构的应用程序,这是没有问题的。但要意识到还有其它情形。

值得指出的是,有一个选项确实允许分别 pickle 对象,并维护相互之间的引用,只要这些对象都是 pickle 到同一文件即可。 pickle 和 cPickle 模块提供了一个 Pickler (与此相对应是 Unpickler ),它能够跟踪已经被 pickle 的对象。通过使用这个 Pickler ,将会通过引用而不是通过值来 pickle 共享和循环引用:

清单 9. 维护分别 pickle 的对象间的引用:

不可 pickle 的对象

一些对象类型是不可 pickle 的。例如,Python 不能 pickle 文件对象(或者任何带有对文件对象引用的对象),因为 Python 在 unpickle 时不能保证它可以重建该文件的状态(另一个示例比较难懂,在这类文章中不值得提出来)。试图 pickle 文件对象会导致以下错误:

清单 10. 试图 pickle 文件对象的结果:

与 pickle 简单对象类型相比,pickle 类实例要多加留意。这主要由于 Python 会 pickle 实例数据(通常是 _dict_ 属性)和类的名称,而不会 pickle 类的代码。当 Python unpickle 类的实例时,它会试图使用在 pickle 该实例时的确切的类名称和模块名称(包括任何包的路径前缀)导入包含该类定义的模块。另外要注意,类定义必须出现在模块的最顶层,这意味着它们不能是嵌套的类(在其它类或函数中定义的类)。

当 unpickle 类的实例时,通常不会再调用它们的 _init_() 方法。相反,Python 创建一个通用类实例,并应用已进行过 pickle 的实例属性,同时设置该实例的 _class_ 属性,使其指向原来的类。

对 Python 2.2 中引入的新型类进行 unpickle 的机制与原来的略有不同。虽然处理的结果实际上与对旧型类处理的结果相同,但 Python 使用 copy_reg 模块的 _reconstructor() 函数来恢复新型类的实例。

如果希望对新型或旧型类的实例修改缺省的 pickle 行为,则可以定义特殊的类的方法 _getstate_() 和 _setstate_() ,在保存和恢复类实例的状态信息期间,Python 会调用这些方法。在以下几节中,我们会看到一些示例利用了这些特殊的方法。

现在,我们看一个简单的类实例。首先,创建一个 persist.py 的 Python 模块,它包含以下新型类的定义:

清单 11. 新型类的定义:

现在可以 pickle Foo 实例,并看一下它的表示:

清单 12. pickle Foo 实例:

可以看到这个类的名称 Foo 和全限定的模块名称 Orbtech.examples.persist 都存储在 pickle 中。如果将这个实例 pickle 成一个文件,稍后再 unpickle 它或在另一台机器上 unpickle,则 Python 会试图导入 Orbtech.examples.persist 模块,如果不能导入,则会抛出异常。如果重命名该类和该模块或者将该模块移到另一个目录,则也会发生类似的错误。

这里有一个 Python 发出错误消息的示例,当我们重命名 Foo 类,然后试图装入先前进行过 pickle 的 Foo 实例时会发生该错误:

您好,如果您已经对某个类实例进行了 pickle,而现在又需要更改这个类,则您可能要检索和更新那些实例,以便它们能在新的类定义下继续正常工作。

要更改类名,而不破坏先前经过 pickle 的实例,请遵循以下步骤。 首先,确保原来的类的定义没有被更改,以便在 unpickle 现有实例时可以找到它。 不要更改原来的名称,而是在与原来类定义所在的同一个模块中, 创建该类定义的一个副本,同时给它一个新的名称。

要更改类名而不破坏先前经过pickle的实例,请遵循以下步骤。首先,确保原来的类定义没有被更改,以便在unpickle现有实例时可以找到它。不要更改原来的名称,而是在与原来类定义所在的同一个模块中,创建该类定义的一个副本,同时给它一个新的类名。然后使用实际的新类名来替代NewClassName,将以下方法添加到原来类的定义中:

清单16. 更改类名:添加到原来类定义的方法:

当unpickle现有实例时,Python将查找原来类的定义,并调用实例的_setstate_()方法,同时将给新的类定义重新分配该实例的_class_属性。一旦确定所有现有的实例都已经unpickle、更新和重新pickle后,可以从源代码模块中除去旧的类定义。

属性的添加和删除

这些特殊的状态方法_getstate_()和_setstate_()再一次使我们能控制每个实例的状态,并使我们有机会处理实例属性中的更改。让我们看一个简单的类的定义,我们将向其添加和除去一些属性。这是是最初的定义:

清单17. 最初的类定义:

假定已经创建并pickle了Person的实例,现在我们决定真的只想存储一个名称属性,而不是分别存储姓和名。这里有一种方式可以更改类的定义,它将先前经过pickle的实例迁移到新的定义:

清单18. 新的类定义:

在这个示例,我们添加了一个新的属性fullname,并除去了两个现有的属性firstname和lastname。当对先前进行过pickle的实例执行unpickle时,其先前进行过pickle的状态会作为字典传递给_setstate_(),它将包括firstname和lastname属性的值。接下来,将这两个值组合起来,并将它们分配给新属性fullname。在这个过程中,我们删除了状态字典中旧的属性。更新和重新pickle先前进行过pickle的所有实例之后,现在可以从类定义中除去_setstate_()方法。

模块的修改

在概念上,模块名称或位置的改变类似于类名称的更改。然而,两者的处理方式完全不同。这是因为模块信息存储在pickle文件中,而非标准的pickle接口可以修改的属性。实际上,要修改模块信息,唯一的方法是对pickle文件本身进行查找和替换操作。具体的操作方法取决于所使用的操作系统和可用工具。

在这种情况下,为了避免出现错误,首先需要对文件进行备份。值得注意的是,这种修改应该是相对简单的,并且对二进制pickle格式和文本pickle格式的更改应该具有相同的效果。总之,尽管模块名称或位置的更改可能会带来一定程度的复杂性,但只要采取适当的措施并遵循正确的步骤,就可以实现这一目标。