永続化とは、同じプログラムを複数回実行してもオブジェクトを保持することを意味します。この記事では、リレーショナル データベースから Python の pickle やその他のメカニズムに至るまで、Python オブジェクトのさまざまな永続化メカニズムの概要を理解します。さらに、Python のオブジェクトシリアル化機能についてもより深く理解できるようになります。
持続性とは何ですか?
永続性の基本的な考え方はシンプルです。 Python プログラム (おそらく毎日の To Do 項目を管理するプログラム) があり、プログラムの実行の間にアプリケーション オブジェクト (To Do 項目) を保存したいとします。つまり、後で簡単に取得できるように、オブジェクトをディスクに保存したいと考えます。これが粘り強さです。これを実現するにはいくつかの方法がありますが、それぞれに長所と短所があります。
たとえば、オブジェクト データは、CSV ファイルなどの特定の形式のテキスト ファイルに保存できます。または、Gadfly、MySQL、PostgreSQL、DB2 などのリレーショナル データベースを使用することもできます。これらのファイル形式とデータベースは優れており、Python にはこれらすべてのストレージ メカニズムへの堅牢なインターフェイスが備わっています。
これらのストレージ メカニズムにはすべて、保存されたデータがオブジェクトやデータ上で動作するプログラムから独立しているという共通点があります。この利点は、データを他のアプリケーションが使用する共有リソースとして使用できることです。欠点は、この方法では、他のプログラムがオブジェクトのデータにアクセスできるようになり、オブジェクト指向のカプセル化原則に違反することです。つまり、オブジェクトのデータには、オブジェクト自身のパブリック インターフェイスを介してのみアクセスできます。
また、一部のアプリケーションでは、リレーショナル データベースのアプローチはあまり理想的ではない可能性があります。特に、リレーショナル データベースはオブジェクトを認識しません。対照的に、リレーショナル データベースは独自の型システムとリレーショナル データ モデル (テーブル) を強制し、各テーブルには一連のタプル (行) が含まれ、各行には固定数の静的に型指定されたフィールド (列) が含まれます。アプリケーションのオブジェクト モデルをリレーショナル モデルに簡単に変換できない場合、オブジェクトをタプルにマッピングしたり、タプルをオブジェクトに戻すことが困難になります。この問題は、多くの場合、インピーダンス不整合問題と呼ばれます。
オブジェクトの永続性
Python オブジェクトの ID や型などの情報を失わずに透過的に保存したい場合は、何らかの形式のオブジェクトのシリアル化が必要です。これは、任意の複雑なオブジェクトをオブジェクト表現プロセスに変換するテキストまたはバイナリです。同様に、オブジェクトのシリアル化された形式を元の形式に復元できなければなりません。 Python では、このシリアル化プロセスは pickle と呼ばれ、オブジェクトを文字列、ディスク上のファイル、または任意のファイルのようなオブジェクトに pickle することができ、これらの文字列、ファイル、または任意のファイルのようなオブジェクトを元のオブジェクトに unpickle することもできます。ピクルスについては、この記事の後半で詳しく説明します。
すべてをオブジェクトとして保存し、オブジェクトを非オブジェクトベースのストレージに変換するオーバーヘッドを回避したいと仮定すると、pickle ファイルはこれらの利点を提供しますが、場合によっては、pickle ファイルよりも堅牢である必要があるかもしれません。この単純なピクルス ファイルと、さらにスケーラブルなもの。たとえば、pickle だけでは、pickle ファイルの名前付けと検索の問題を解決できません。また、永続オブジェクトへの同時アクセスもサポートしません。これらの機能が必要な場合は、ZODB (Z Object Database for Python) などのデータベースを利用する必要があります。 ZODB は、堅牢なマルチユーザーのオブジェクト指向データベース システムであり、任意の複雑さの Python オブジェクトを保存および管理でき、トランザクション操作と同時実行制御をサポートします。 (ZODB をダウンロードするには、「参考文献」を参照してください。) 興味深いことに、ZODB ですら Python のネイティブのシリアル化機能に依存しており、ZODB を効果的に使用するには、pickle についてよく理解する必要があります。
永続性の問題に対するもう 1 つの興味深い解決策は、もともと Java で実装された Prevayler です (Prevayler に関するdeveloperWorks の記事については「参考文献」を参照)。最近、Python プログラマーのグループが Prevayler を Python に移植し、PyPerSyst という名前に変更して、SourceForge でホストしました (PyPerSyst プロジェクトへのリンクについては「参考文献」を参照)。 Prevayler/PyPerSyst の概念も、Java および Python 言語のネイティブのシリアル化機能に基づいて構築されています。 PyPerSyst は、オブジェクト システム全体をメモリ内に保持し、システム スナップショットをディスクに時々ピクルし、最新のスナップショットを再適用できるコマンド ログを維持することで災害復旧を実現します。したがって、PyPerSyst を使用するアプリケーションは利用可能なメモリによって制限されますが、利点は、ネイティブ オブジェクト システムがメモリに完全に収まり、ZODB のようなデータベースよりも実装が非常に高速かつ簡単になることです。同時にメモリに保持することができます。
永続オブジェクトを保存するさまざまな方法について簡単に説明しましたが、ここで pickle プロセスを詳しく調べてみましょう。私たちは主に、Python オブジェクトを他の形式に変換せずに保存するさまざまな方法を検討することに興味がありますが、単純なオブジェクトとインスタンスを含む複雑なオブジェクトを効率的にピクルおよびピクル解除する方法など、まだ検討すべき点がいくつかあります。カスタム クラスの説明、循環参照や再帰参照を含むオブジェクトへの参照を維持する方法、以前にピクルされたインスタンスを問題なく使用できるようにクラス定義への変更を処理する方法。これらすべての問題については、後の Python の pickle 機能の説明で説明します。
いくつかの 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() 関数を使用した対話型セッションを示しています。は文字列です', 42, [1, 2, 3], None)
>>> p1 = pickle.dumps(t1)
>>> p1
"(これは文字列'/nI42/n(lp1/nI1/naI2/naI3/naNtp2/n."
>>> print p1
(S'これは文字列です'
I42
(lp1
I1
aI2
aI3
aNtp2
>>> t2 = pickle.loads(p1)
>>>文字列'、42、 [1, 2, 3], なし)
>>> p2 = pickle.dumps(t1, True)
>>> p2
'(U/x10 これは文字列です K*]q /x01(K/x01K/x02K/x03eNtq/x02.'
>>> t3 = pickle.loads(p2)
>>> t3
('これは文字列です', 42 、[1、2、3]、なし)
注: テキストの pickle 形式は非常に単純なので、ここでは説明しません。実際には、pickle モジュールで使用されるすべての規則も記録されていることに注意してください。この例では単純なオブジェクトを使用しているため、バイナリの pickle 形式を使用してもスペースの節約という点ではそれほど効率的ではありませんが、複雑なオブジェクトを使用する実際のシステムでは、バイナリ形式を使用するとサイズと速度が大幅に向上することがわかります。
次に、ファイルとファイルのようなオブジェクトを操作する dump() と load() を使用した例をいくつか見ていきます。もう 1 つの機能 - dump() 関数は、複数のオブジェクトを同じファイルに次々にダンプしてから、load() を呼び出して、これらのオブジェクトを同じ順序で取得できます。リスト 2 は、この機能の実際の動作を示しています。
リスト 2. dump() と load() の例
>>> import cPickle as pickle >>> t1 = ('this is a string', 42, [1, 2, 3], None)
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 1 >>> b[2] is a 1 >>> 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 1 >>> d[2] is c 1
注意,如果分别 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 0 >>> d[2] is c 0
相等,但并不总是相同
正如在上一个示例所暗示的,只有在这些对象引用内存中同一个对象时,它们才是相同的。在 pickle 情形中,每个对象被恢复到一个与原来对象相等的对象,但不是同一个对象。换句话说,每个 pickle 都是原来对象的一个副本:
清单 8. 作为原来对象副本的被恢复的对象
>>> j = [1, 2, 3] >>> k = j >>> k is j 1 >>> x = pickle.dumps(k) >>> y = pickle.loads(x) >>> y [1, 2, 3] >>> y == k 1 >>> y is k 0 >>> y is j 0 >>> k is j 1
同时,我们看到 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 1 >>> d[2] is c 1
不可 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 "<input>", 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 "<input>", 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 Foo 的实例时,Python 将只 pickle 当它调用该实例的 _getstate_() 方法时返回给它的值。类似的,在 unpickle 时,Python 将提供经过 unpickle 的值作为参数传递给实例的 _setstate_() 方法。在 _setstate_() 方法内,可以根据经过 pickle 的名称和位置信息来重建文件对象,并将该文件对象分配给这个实例的 logfile 属性。
模式改进
随着时间的推移,您会发现自己必须要更改类的定义。如果已经对某个类实例进行了 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 的实例迁移到新的定义:
清单 18. 新的类定义
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)
在这个示例,我们添加了一个新的属性 fullname ,并除去了两个现有的属性 firstname 和 lastname 。当对先前进行过 pickle 的实例执行 unpickle 时,其先前进行过 pickle 的状态会作为字典传递给 _setstate_() ,它将包括 firstname 和 lastname 属性的值。接下来,将这两个值组合起来,并将它们分配给新属性 fullname 。在这个过程中,我们删除了状态字典中旧的属性。更新和重新 pickle 先前进行过 pickle 的所有实例之后,现在可以从类定义中除去 _setstate_() 方法。
模块的修改
在概念上,模块的名称或位置的改变类似于类名称的改变,但处理方式却完全不同。那是因为模块的信息存储在 pickle 中,而不是通过标准的 pickle 接口就可以修改的属性。事实上,改变模块信息的唯一办法是对实际的 pickle 文件本身执行查找和替换操作。至于如何确切地去做,这取决于具体的操作系统和可使用的工具。很显然,在这种情况下,您会想备份您的文件,以免发生错误。但这种改动应该非常简单,并且对二进制 pickle 格式进行更改与对文本 pickle 格式进行更改应该一样有效。
更多python pickle模块相关文章请关注PHP中文网!