永続化とは、同じプログラムを複数回実行してもオブジェクトを保持することを意味します。この記事では、リレーショナル データベースから Python の pickle やその他のメカニズムに至るまで、Python オブジェクトのさまざまな永続化メカニズムの概要を理解します。さらに、Python のオブジェクトシリアル化機能についてもより深く理解できるようになります。
持続性とは何ですか?
永続性の基本的な考え方はシンプルです。 Python プログラム (おそらく毎日の To Do 項目を管理するプログラム) があり、プログラムの実行の間にアプリケーション オブジェクト (To Do 項目) を保存したいとします。つまり、オブジェクトをディスクに保存して、後で取得できるようにしたいと考えます。これが粘り強さです。これを実現するにはいくつかの方法がありますが、それぞれに長所と短所があります。
たとえば、オブジェクト データは、CSV ファイルなどの特定の形式のテキスト ファイルに保存できます。または、Gadfly、MySQL、PostgreSQL、DB2 などのリレーショナル データベースを使用することもできます。これらのファイル形式とデータベースは優れており、Python にはこれらすべてのストレージ メカニズムへの堅牢なインターフェイスが備わっています。
これらのストレージ メカニズムにはすべて、保存されたデータが、これらのデータを操作するオブジェクトやプログラムから独立しているという共通点があります。この利点は、データを他のアプリケーションが使用する共有リソースとして使用できることです。欠点は、この方法では、他のプログラムがオブジェクトのデータにアクセスできるようになり、オブジェクト指向のカプセル化原則に違反することです。つまり、オブジェクトのデータには、オブジェクト自身のパブリック インターフェイスを介してのみアクセスできます。
また、一部のアプリケーションでは、リレーショナル データベースのアプローチはあまり理想的ではない可能性があります。特に、リレーショナル データベースはオブジェクトを認識しません。対照的に、リレーショナル データベースは独自の型システムとリレーショナル データ モデル (テーブル) を強制し、各テーブルには一連のタプル (行) が含まれ、各行には固定数の静的に型指定されたフィールド (列) が含まれます。アプリケーションのオブジェクト モデルをリレーショナル モデルに簡単に変換できない場合、オブジェクトをタプルにマッピングしたり、タプルをオブジェクトに戻すことが困難になります。この問題は、多くの場合、インピーダンス不整合問題と呼ばれます。
いくつかの Python の pickle
pickle モジュールとそのいとこである cPickle は、Python に pickle サポートを提供します。後者は C でコーディングされており、パフォーマンスが優れているため、ほとんどのアプリケーションにはこのモジュールが推奨されます。 pickle については引き続き説明しますが、この記事の例では実際に cPickle を使用しています。これらの例のほとんどは Python シェルを使用して示されるため、最初に cPickle をインポートし、それを pickle として参照する方法を示しましょう:
>>> import cPickle as pickle
モジュールがインポートされたので、pickle インターフェイスを見てみましょう。 pickle モジュールは次の関数ペアを提供します: dumps(object) はオブジェクトを含む文字列を pickle 形式で返します;loads(string) は pickle 文字列に含まれるオブジェクトを返します; dump(object, file) はオブジェクトをファイルに書き込みます。 file は実際の物理ファイルにすることもできますが、任意のファイルのようなオブジェクトにすることもできます。このオブジェクトには単一の文字列パラメータを受け入れる write() メソッドがあり、load(file) は pickle ファイルに含まれるオブジェクトを返します。
デフォルトでは、dumps() と dump() は印刷可能な ASCII 表現を使用して pickle を作成します。どちらもオプションの最終パラメータを取り、 True の場合、ピクルがより高速で小さいバイナリ表現で作成されることを指定します。 loads() 関数とload() 関数は、pickle がバイナリ形式であるかテキスト形式であるかを自動的に検出します。
リスト 1 は、先ほど説明した dumps() 関数と loads() 関数を使用した対話型セッションを示しています。
リスト 1. dumps() と loads() のデモ
Welcome To PyCrust 0.7.2 - The Flakiest Python Shell Sponsored by Orbtech - Your source for Python programming expertise. Python 2.2.1 (#1, Aug 27 2002, 10:22:32) [GCC 3.2 (Mandrake Linux 9.0 3.2-1mdk)] on linux-i386 Type "copyright", "credits" or "license" for more information. >>> import cPickle as pickle >>> t1 = ('this is a string', 42, [1, 2, 3], None) >>> t1 ('this is a string', 42, [1, 2, 3], None) >>> p1 = pickle.dumps(t1) >>> p1 "(S'this is a string'\nI42\n(lp1\nI1\naI2\naI3\naNtp2\n." >>> print p1 (S'this is a string' I42 (lp1 I1 aI2 aI3 aNtp2 . >>> t2 = pickle.loads(p1) >>> t2 ('this is a string', 42, [1, 2, 3], None) >>> p2 = pickle.dumps(t1, True) >>> p2 '(U\x10this is a stringK*]q\x01(K\x01K\x02K\x03eNtq\x02.' >>> t3 = pickle.loads(p2) >>> t3 ('this is a string', 42, [1, 2, 3], None)
注: このテキストはピクル化されています 形式は非常に単純ですここでは説明しません。実際、使用されるすべての規則は pickle モジュールに文書化されています。また、この例では単純なオブジェクトを使用しているため、バイナリの pickle 形式を使用してもスペースの節約という点ではあまり効率的ではないことにも注意してください。ただし、複雑なオブジェクトを使用する実際のシステムでは、バイナリ形式を使用するとサイズと速度が大幅に向上することがわかります。
次に、ファイルとファイルに似たオブジェクトを扱う dump() と load() を使用した例をいくつか見てみましょう。これらの関数の動作は、先ほど説明した dumps() およびloads() と非常に似ていますが、dump() 関数には複数のオブジェクトを同じファイルに次々にダンプできるという別の機能があります。後続のload()の呼び出しでは、これらのオブジェクトが同じ順序で取得されます。リスト 2 は、この機能の実際の動作を示しています。
リスト 2. dump() と load() の例
>>> a1 = 'apple' >>> b1 = {1: 'One', 2: 'Two', 3: 'Three'} >>> c1 = ['fee', 'fie', 'foe', 'fum'] >>> f1 = file('temp.pkl', 'wb') >>> pickle.dump(a1, f1, True) >>> pickle.dump(b1, f1, True) >>> pickle.dump(c1, f1, True) >>> f1.close() >>> f2 = file('temp.pkl', 'rb') >>> a2 = pickle.load(f2) >>> a2 'apple' >>> b2 = pickle.load(f2) >>> b2 {1: 'One', 2: 'Two', 3: 'Three'} >>> c2 = pickle.load(f2) >>> c2 ['fee', 'fie', 'foe', 'fum'] >>> f2.close()
Pickle のパワー
ここまで、pickle の基本について説明してきました。このセクションでは、カスタム クラスのインスタンスなど、複雑なオブジェクトのピクルスを開始するときに発生するいくつかの高度な問題について説明します。幸いなことに、Python はこの状況を簡単に処理できます。
携帯性
从 空间和时间上说,Pickle 是可移植的。换句话说,pickle 文件格式独立于机器的体系结构,这意味着,例如,可以在 Linux 下创建一个 pickle,然后将它发送到在 Windows 或 Mac OS 下运行的 Python 程序。并且,当升级到更新版本的 Python 时,不必担心可能要废弃已有的 pickle。Python 开发人员已经保证 pickle 格式将可以向后兼容 Python 各个版本。事实上,在 pickle 模块中提供了有关目前以及所支持的格式方面的详细信息:
清单 3. 检索所支持的格式
>>> pickle.format_version '1.3' >>> pickle.compatible_formats ['1.0', '1.1', '1.2']
多个引用,同一对象
在 Python 中,变量是对象的引用。同时,也可以用多个变量引用同一个对象。经证明,Python 在用经过 pickle 的对象维护这种行为方面丝毫没有困难,如清单 4 所示:
清单 4. 对象引用的维护
>>> a = [1, 2, 3] >>> b = a >>> a [1, 2, 3] >>> b [1, 2, 3] >>> a.append(4) >>> a [1, 2, 3, 4] >>> b [1, 2, 3, 4] >>> c = pickle.dumps((a, b)) >>> d, e = pickle.loads(c) >>> d [1, 2, 3, 4] >>> e [1, 2, 3, 4] >>> d.append(5) >>> d [1, 2, 3, 4, 5] >>> e [1, 2, 3, 4, 5]
循环引用和递归引用
可以将刚才演示过的对象引用支持扩展到 循环引用(两个对象各自包含对对方的引用)和 递归引用(一个对象包含对其自身的引用)。下面两个清单着重显示这种能力。我们先看一下递归引用:
>清单 5. 递归引用
>>> l = [1, 2, 3] >>> l.append(l) >>> l [1, 2, 3, [...]] >>> l[3] [1, 2, 3, [...]] >>> l[3][3] [1, 2, 3, [...]] >>> p = pickle.dumps(l) >>> l2 = pickle.loads(p) >>> l2 [1, 2, 3, [...]] >>> l2[3] [1, 2, 3, [...]] >>> l2[3][3] [1, 2, 3, [...]]
现在,看一个循环引用的示例:
清单 6. 循环引用
>>> a = [1, 2] >>> b = [3, 4] >>> a.append(b) >>> a [1, 2, [3, 4]] >>> b.append(a) >>> a [1, 2, [3, 4, [...]]] >>> b [3, 4, [1, 2, [...]]] >>> a[2] [3, 4, [1, 2, [...]]] >>> b[2] [1, 2, [3, 4, [...]]] >>> a[2] is b >>> b[2] is a >>> f = file('temp.pkl', 'w') >>> pickle.dump((a, b), f) >>> f.close() >>> f = file('temp.pkl', 'r') >>> c, d = pickle.load(f) >>> f.close() >>> c [1, 2, [3, 4, [...]]] >>> d [3, 4, [1, 2, [...]]] >>> c[2] [3, 4, [1, 2, [...]]] >>> d[2] [1, 2, [3, 4, [...]]] >>> c[2] is d >>> d[2] is c
注意,如果分别 pickle 每个对象,而不是在一个元组中一起 pickle 所有对象,会得到略微不同(但很重要)的结果,如清单 7 所示:
清单 7. 分别 pickle vs. 在一个元组中一起 pickle
>>> f = file('temp.pkl', 'w') >>> pickle.dump(a, f) >>> pickle.dump(b, f) >>> f.close() >>> f = file('temp.pkl', 'r') >>> c = pickle.load(f) >>> d = pickle.load(f) >>> f.close() >>> c [1, 2, [3, 4, [...]]] >>> d [3, 4, [1, 2, [...]]] >>> c[2] [3, 4, [1, 2, [...]]] >>> d[2] [1, 2, [3, 4, [...]]] >>> c[2] is d >>> d[2] is c
相等,但并不总是相同
正如在上一个示例所暗示的,只有在这些对象引用内存中同一个对象时,它们才是相同的。在 pickle 情形中,每个对象被恢复到一个与原来对象相等的对象,但不是同一个对象。换句话说,每个 pickle 都是原来对象的一个副本:
清单 8. 作为原来对象副本的被恢复的对象
>>> j = [1, 2, 3] >>> k = j >>> k is j >>> x = pickle.dumps(k) >>> y = pickle.loads(x) >>> y [1, 2, 3] >>> y == k >>> y is k >>> y is j >>> k is j
同时,我们看到 Python 能够维护对象之间的引用,这些对象是作为一个单元进行 pickle 的。然而,我们还看到分别调用 dump() 会使 Python 无法维护对在该单元外部进行 pickle 的对象的引用。相反,Python 复制了被引用对象,并将副本和被 pickle 的对象存储在一起。对于 pickle 和恢复单个对象层次结构的应用程序,这是没有问题的。但要意识到还有其它情形。
值得指出的是,有一个选项确实允许分别 pickle 对象,并维护相互之间的引用,只要这些对象都是 pickle 到同一文件即可。 pickle 和 cPickle 模块提供了一个 Pickler (与此相对应是 Unpickler ),它能够跟踪已经被 pickle 的对象。通过使用这个 Pickler ,将会通过引用而不是通过值来 pickle 共享和循环引用:
清单 9. 维护分别 pickle 的对象间的引用
>>> f = file('temp.pkl', 'w') >>> pickler = pickle.Pickler(f) >>> pickler.dump(a) <cPickle.Pickler object at 0x89b0bb8> >>> pickler.dump(b) <cPickle.Pickler object at 0x89b0bb8> >>> f.close() >>> f = file('temp.pkl', 'r') >>> unpickler = pickle.Unpickler(f) >>> c = unpickler.load() >>> d = unpickler.load() >>> c[2] [3, 4, [1, 2, [...]]] >>> d[2] [1, 2, [3, 4, [...]]] >>> c[2] is d >>> d[2] is c
不可 pickle 的对象
一 些对象类型是不可 pickle 的。例如,Python 不能 pickle 文件对象(或者任何带有对文件对象引用的对象),因为 Python 在 unpickle 时不能保证它可以重建该文件的状态(另一个示例比较难懂,在这类文章中不值得提出来)。试图 pickle 文件对象会导致以下错误:
清单 10. 试图 pickle 文件对象的结果
>>> f = file('temp.pkl', 'w') >>> p = pickle.dumps(f) Traceback (most recent call last): File "<input>", line 1, in ? File "/usr/lib/python2.2/copy_reg.py", line 57, in _reduce raise TypeError, "can't pickle %s objects" % base.__name__ TypeError: can't pickle file objects
类实例
与 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. 新型类的定义
class Foo(object): def __init__(self, value): self.value = value
现在可以 pickle Foo 实例,并看一下它的表示:
清单 12. pickle Foo 实例
>>> import cPickle as pickle >>> from Orbtech.examples.persist import Foo >>> foo = Foo('What is a Foo?') >>> p = pickle.dumps(foo) >>> print p ccopy_reg _reconstructor p1 (cOrbtech.examples.persist Foo p2 c__builtin__ object p3 NtRp4 (dp5 S'value' p6 S'What is a Foo?' sb. >>>
可以看到这个类的名称 Foo 和全限定的模块名称 Orbtech.examples.persist 都存储在 pickle 中。如果将这个实例 pickle 成一个文件,稍后再 unpickle 它或在另一台机器上 unpickle,则 Python 会试图导入 Orbtech.examples.persist 模块,如果不能导入,则会抛出异常。如果重命名该类和该模块或者将该模块移到另一个目录,则也会发生类似的错误。
这里有一个 Python 发出错误消息的示例,当我们重命名 Foo 类,然后试图装入先前进行过 pickle 的 Foo 实例时会发生该错误:
清单 13. 试图装入一个被重命名的 Foo 类的经过 pickle 的实例
>>> import cPickle as pickle >>> f = file('temp.pkl', 'r') >>> foo = pickle.load(f) Traceback (most recent call last): File "", line 1, in ? AttributeError: 'module' object has no attribute 'Foo'
在重命名 persist.py 模块之后,也会发生类似的错误:
清单 14. 试图装入一个被重命名的 persist.py 模块的经过 pickle 的实例
>>> import cPickle as pickle >>> f = file('temp.pkl', 'r') >>> foo = pickle.load(f) Traceback (most recent call last): File "", line 1, in ? ImportError: No module named persist
我们会在下面 模式改进这一节提供一些技术来管理这类更改,而不会破坏现有的 pickle。
特殊的状态方法
前面提到对一些对象类型(譬如,文件对象)不能进行 pickle。处理这种不能 pickle 的对象的实例属性时可以使用特殊的方法( _getstate_() 和 _setstate_() )来修改类实例的状态。这里有一个 Foo 类的示例,我们已经对它进行了修改以处理文件对象属性:
清单 15. 处理不能 pickle 的实例属性
class Foo(object): def __init__(self, value, filename): self.value = value self.logfile = file(filename, 'w') def __getstate__(self): """Return state values to be pickled.""" f = self.logfile return (self.value, f.name, f.tell()) def __setstate__(self, state): """Restore state from the unpickled state values.""" self.value, name, position = state f = file(name, 'w') f.seek(position) self.logfile = f
模式改进
随 着时间的推移,您会发现自己必须要更改类的定义。如果已经对某个类实例进行了 pickle,而现在又需要更改这个类,则您可能要检索和更新那些实例,以便它们能在新的类定义下继续正常工作。而我们已经看到在对类或模块进行某些更改 时,会出现一些错误。幸运的是,pickle 和 unpickle 过程提供了一些 hook,我们可以用它们来支持这种模式改进的需要。
在 这一节,我们将探讨一些方法来预测常见问题以及如何解决这些问题。由于不能 pickle 类实例代码,因此可以添加、更改和除去方法,而不会影响现有的经过 pickle 的实例。出于同样的原因,可以不必担心类的属性。您必须确保包含类定义的代码模块在 unpickle 环境中可用。同时还必须为这些可能导致 unpickle 问题的更改做好规划,这些更改包括:更改类名、添加或除去实例的属性以及改变类定义模块的名称或位置。
类名的更改
要 更改类名,而不破坏先前经过 pickle 的实例,请遵循以下步骤。首先,确保原来的类的定义没有被更改,以便在 unpickle 现有实例时可以找到它。不要更改原来的名称,而是在与原来类定义所在的同一个模块中,创建该类定义的一个副本,同时给它一个新的类名。然后使用实际的新类 名来替代 NewClassName ,将以下方法添加到原来类的定义中:
清单 16. 更改类名:添加到原来类定义的方法
def __setstate__(self, state): self.__dict__.update(state) self.__class__ = NewClassName
当 unpickle 现有实例时,Python 将查找原来类的定义,并调用实例的 _setstate_() 方法,同时将给新的类定义重新分配该实例的 _class_ 属性。一旦确定所有现有的实例都已经 unpickle、更新和重新 pickle 后,可以从源代码模块中除去旧的类定义。
属性的添加和删除
这些特殊的状态方法 _getstate_() 和 _setstate_() 再一次使我们能控制每个实例的状态,并使我们有机会处理实例属性中的更改。让我们看一个简单的类的定义,我们将向其添加和除去一些属性。这是是最初的定义:
清单 17. 最初的类定义
class Person(object): def __init__(self, firstname, lastname): self.firstname = firstname self.lastname = lastname
假定已经创建并 pickle 了 Person 的实例,现在我们决定真的只想存储一个名称属性,而不是分别存储姓和名。这里有一种方式可以更改类的定义,它将先前经过 pickle 的实例迁移到新的定义:
class Person(object): def __init__(self, fullname): self.fullname = fullname def __setstate__(self, state): if 'fullname' not in state: first = '' last = '' if 'firstname' in state: first = state['firstname'] del state['firstname'] if 'lastname' in state: last = state['lastname'] del state['lastname'] self.fullname = " ".join([first, last]).strip() self.__dict__.update(state)
模块的修改
在概念上,模块的名称或位置的改变类似于类名称的改变,但处理方式却完全不同。那是因为模块的信息存储在 pickle 中,而不是通过标准的 pickle 接口就可以修改的属性。事实上,改变模块信息的唯一办法是对实际的 pickle 文件本身执行查找和替换操作。至于如何确切地去做,这取决于具体的操作系统和可使用的工具。很显然,在这种情况下,您会想备份您的文件,以免发生错误。但 这种改动应该非常简单,并且对二进制 pickle 格式进行更改与对文本 pickle 格式进行更改应该一样有效。
结束语
オブジェクトの永続性は、基礎となるプログラミング言語のオブジェクトのシリアル化機能に依存します。 Python オブジェクトの場合、これはピクルを意味します。 Python の pickle は、Python オブジェクトの効率的な永続性管理のための堅牢で信頼性の高い基盤を提供します。以下のリソースでは、Python の pickle 機能に基づいて構築されたシステムに関する情報が見つかります。
Python 永続管理 pickle モジュールの詳細な紹介と関連記事については、PHP 中国語 Web サイトに注目してください。