Pythonでの永続性管理

シリアライゼーションによるPythonオブジェクトの保存

永続性 (persistence) とは、プログラムの実行の間もオブジェクトを存続させることです。本稿によって、リレーショナル・データベースからPythonのピクルなどまで、Pythonオブジェクトのいろいろな永続性のメカニズムについて基本的なことを理解できます。また、Pythonのオブジェクト・シリアライゼーション機能についても理解を深めることができるでしょう。

Patrick O’Brien (pobrien@orbtech.com), Python programmer, Orbtech

Patrick O’BrienPatrick O'Brienは、Pythonプログラマーで、コンサルタントと講師をやっています。PyCrustの作者であり、PythonCardプロジェクトの開発にも参加しています。一番最近では、PrevaylerをPythonに移植したPyPerSystチームを指揮しており、引き続き、このプロジェクトを率い、面白い別の分野に取り組んでいます。彼のメール・アドレスはpobrien@orbtech.com です。



2002年 11月 01日

永続性とは

永続性の基本的な考え方は、いたって簡単です。ここにPythonプログラムがあるとします。たとえば、毎日の行動計画を管理するためのプログラムなどです。このプログラムを実行してから、次にそれを実行するまでの間、アプリケーション・オブジェクト (行動計画データ) を保存しておく必要があります。言い換えれば、オブジェクトをディスクに保存し、後でそれを取り出したいのです。これが、永続性ということです。これを実現するには、いろいろな方法がありますが、それぞれに一長一短があります。

たとえば、オブジェクト・データを、CSVファイルなど、何らかの形式のテキスト・ファイルに保存することもできるでしょう。あるいは、Gadfly、MySQL、PostgreSQL、DB2などのリレーショナル・データベースを利用することもできるでしょう。これらのファイル・フォーマットやデータベースは、充分確立されたものであり、Pythonは、それらの記憶保存メカニズム (storage mechanisms) すべてについて、堅牢なインターフェースを用意しています。

これらすべての記憶保存メカニズムに共通していることとして、データが、オブジェクトとは無関係に、あるいはそれらのデータを処理するプログラムとは無関係に、保存されるということがあります。これは、データを他のアプリケーションとの間で共有資源として利用できるという点では利点です。欠点は、オブジェクトのデータをこのようにアクセスできるようにするということが、オブジェクト指向のカプセル化の原理に背いているという点です。オブジェクトのデータは、そのオブジェクトのパブリックなインターフェースを通してのみアクセスされるようにしなければならないというのが、カプセル化の原理でした。

そうすると、アプリケーションによっては、リレーショナル・データベースを使う方法は理想的ではないことになるかもしれません。とくに、リレーショナル・データベースの場合、オブジェクトを理解することがないからです。その代わり、リレーショナル・データベースでは、独自の型システムや関係に関するデータ・モデル (テーブル) に従うことが求められます。各テーブルは、ある一定の数の静的に型設定されるフィールド (列) で構成される一群のタプル (行) からなります。アプリケーションのオブジェクト・モデルをリレーショナル・モデルに簡単に変換できない場合には、オブジェクトをタプルに割り当て、それをまた元に戻してやるという、かなりやっかいな作業を行わなければならないことになります。このようなやっかいな作業は、よくインピーダンス・ミスマッチの問題と言われています。


オブジェクトの永続性

Pythonのオブジェクトを、そのアイデンティティーや型などを失うことなく、透過的に保存したいのであれば、何らかの形のオブジェクト・シリアライゼーション、すなわち、どんな複雑なオブジェクトであろうが、それをテキスト形式またはバイナリー形式に変換する処理が必要となります。同様に、シリアライズされた形式のオブジェクトを、元と同じオブジェクトに復元できる必要もあります。Pythonでは、シリアライゼーション処理のことをピクル処理 (pickling) といい、オブジェクトを、ディスク上の文字列やファイル、あるいはファイルに類した任意のオブジェクト (file-like object) との間で、ピクル / アンピクル (unpickle) することができます。ピクル処理については、後で詳しく説明します。

さて、すべてのものをオブジェクトとして保存するのはよいが、オブジェクトを何らかのオブジェクト以外の記憶形式に変換するためのオーバーヘッドは避けたいという考え方に沿って話を進めます。ピクル・ファイルには、上記のような利点があるわけですが、単純なピクル・ファイルよりも、さらに堅牢で拡張性のあるものにしたい場合があります。たとえば、ピクル処理だけでは、ピクル・ファイルの名前や保存場所を決定する問題を解決できませんし、永続的なオブジェクトへの同時並行的なアクセスもサポートできません。こうした機能が必要なら、Python用のZオブジェクト・データベースであるZODBのようなものを使う必要があります。ZODBは、どんな複雑なPythonオブジェクトでも保存、管理することができ、トランザクションのサポートや並行性の制御も可能な、堅牢なオブジェクト指向のマルチユーザー・データベース・システムです。(ZODBをダウンロードするためのリンク先は、参考文献参照) 面白いことに、ZODBもPython本来のシリアライゼーション機能を利用しており、ZODBを効果的に利用するには、ピクル処理をしっかり理解しておく必要があります。

永続性の問題に対する別の方法として、もともとJavaで実装されたものですが、Prevaylerと呼ばれる面白い手法もあります (参考文献 に、PrevaylorについてのdeveloperWorks の記事を掲げておきます)。Prevaylerは、最近Pythonプログラマーのグループによって移植され、それがPyPerSystという名前で、SourceForgeに収録されています(PyPerSystプロジェクトへのリンク先は、参考文献参照)。Prevayler/PyPerSystの考え方は、JavaやPythonの言語がもともと備えているシリアライゼーション機能をベースにしています。PyPerSystは、オブジェクト・システム全体をメモリー上に置き、ときどきシステムのスナップショットをディスクにピクルしたり、コマンドのログを保守しておき、それを最新のスナップショットに適用し直せるようにすることで、障害回復を行えるようにしています。そのため、PyPerSystを利用するアプリケーションは、RAMの空き容量に制約されることになりますが、メモリー上にオブジェクト・システム全体をそのままロードできるため、極めて高速に処理でき、メモリー上に一度に保有できる以上の量のオブジェクトをサポートする ZODBなどの方式よりも、はるかに簡単に実装することができます。

以上、永続的なオブジェクトを保存するためのいろいろな方法について、ざっと概観してきましたが、今度は、ピクル処理について詳しく調べていきたいと思います。われわれの主たる関心は、Pythonのオブジェクトを、何か別のフォーマットに変換することなく永続させる方法を探ることにあるわけですが、まだいろいろな問題も残っています。単純なオブジェクトも、カスタム・クラスのインスタンスなどの複雑なオブジェクトも効果的にピクル / アンピクルする方法、循環参照や再帰参照を含めたオブジェクトの参照を維持する方法、さらには、クラス定義に対する変更を、以前にピクル化したインスタンスとの間で問題を起こすことなく処理する方法、などの問題です。これらの問題については、以下でPythonのピクル処理の機能を調べていく中で取り上げていきたいと思います。


ピクルされたPythonの味見

Pythonのピクル処理は、pickle モジュールと、その親戚のcPickle モジュールによってサポートされています。後者は、性能を上げるためにCでコーディングされたもので、たいていのアプリケーションでは、こちらのほうを使うとよいでしょう。以下では、pickle で話を進めますが、実際にサンプル・コードで使っているのはcPickle です。以下のサンプル・コードは、ほとんどがPythonシェルで実行されていますので、まずはcPickle をインポートし、それをpickle と呼ぶことができるようにする方法を示しておきます。

>>> import cPickle as pickle

モジュールをインポートしましたので、次にピクルのインターフェースを確認しておきます。pickle モジュールには、次の関数ペアが用意されています。dumps(object) は、オブジェクトをピクル・フォーマットにした文字列を返します。loads(string) は、ピクル文字列に含まれているオブジェクトを返します。dump(object, file) は、オブジェクトをファイルに書き出します。このファイルは、実際の物理的なファイルとすることもできますが、文字列引数を1個とるwrite() メソッドを装備するファイルに類するオブジェクトであれば、どんなものでもかまいません。load(file) は、ピクル・ファイルに含まれているオブジェクトを返します。

デフォルトで、dumps()dump() は、印刷可能なASCII形式でピクルを作成します。どちらの関数も、最後の引数にオプション引数をとり、それがTrue なら、高速でサイズの小さいバイナリー形式でピクルを作成することを指定します。loads()load() の2つの関数は、ピクルがバイナリー形式なのかテキスト形式なのかを自動的に判別します。

リスト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 モジュール中に記載されています。また、上のサンプルに示した簡単なオブジェクトの場合、バイナリーのピクル・フォーマットを使っても、大したスペース効率が得られていないことがわかります。しかし、複雑なオブジェクトを使用する実際のシステムでは、バイナリー・フォーマットにすると、サイズと速度が歴然と改善されるのがわかります。

次に、ファイルやファイルに類するオブジェクトを扱うdump()load() を使ったサンプルをいくつか見ておきます。これらの関数は、いま見たdumps()loads() とほとんど同じような行動をしますが、機能が1つ増えています。dump() 関数では、複数のオブジェクトを次から次へと同じファイルにダンプできるようになっています。load() は、続けて呼び出すと、dump() 関数と同じ順番で、オブジェクトを読み出してきます。リスト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()

ピクルのパワー

これまでは、ピクル処理の基本を紹介しましたが、以下では、カスタム・クラスのインスタンスなどの複雑なオブジェクトをピクルしようとするときに持ち上がってくる高度な問題をいくつか取り上げたいと思います。幸い、Pythonがそうした状況をかなりスムーズに対処してくれることがわかることと思います。

移植性

ピクルは、時や場所によらない移植性があります。すなわち、ピクル・ファイルのフォーマットは、マシン・アーキテクチャーとは独立であり、たとえばLinux下で作成したピクルを、WindowsやMac OS下で実行されるPythonプログラムに送るといったことが可能です。また、新しいバージョンのPythonにアップグレードした場合でも、作成済みのピクルを使えなくなるのではないかと心配する必要はありません。Pythonの開発者陣は、Pythonのバージョン間でピクル・フォーマットの下位方向の互換性を確保することを保証しています。実際、現在のフォーマットおよびサポートされているフォーマットの詳細を、以下のように、pickle モジュールで知ることができます。

リスト3. サポートされているフォーマットの読み出し
>>> pickle.format_version
'1.3'
>>> pickle.compatible_formats
['1.0', '1.1', '1.2']

複数の参照、同じオブジェクト

Pythonの変数は、オブジェクトへの参照になっており、同じオブジェクトを参照する複数の変数を設けることができます。ピクルされたオブジェクトでこの振る舞いを維持することについては、リスト4が示すように、Pythonに何の問題もないことがわかります。

リスト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]

循環参照と再帰参照

いま見たオブジェクト参照のサポートは、2つのオブジェクトがお互いの参照を含む循環参照 およびオブジェクトがそれ自身への参照を含む再帰参照 にも拡張されます。以下の2つのリストは、この機能に焦点を当てたものです。まず、再帰参照のほうから見てみることにします。

リスト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

リスト7に示すように、1個のタプル中にオブジェクトをいっしょにピクルするのではなく、それぞれのオブジェクトを別々にピクルすると、わずかな違いながら、意味の大きく異なる結果になることがわかります。

リスト7. 別々にピクルする場合と1個のタプルにいっしょにピクルする場合
>>> 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

等しいが、必ずしも同じとはかぎらない

上のサンプルから察しがつくように、オブジェクトは、メモリー上の同じオブジェクトを参照している場合にかぎって同じものとなります。ピクルの場合、それぞれのピクルが、元のオブジェクトに等しいが、同一ではないオブジェクトに復元されます。すなわち、それぞれのピクルは、元のオブジェクトのコピーとなっています (リスト8)。

リスト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では、1単位としてピクルされたオブジェクト同士の参照を維持できることもわかりました。ただし、dump() を別々に呼び出すと、ピクルされている単位外のオブジェクトへの参照を維持するPythonの機能は無効となることもわかりました。その代わりに、Pythonは、参照されているオブジェクトのコピーを作成し、ピクルしようとしている項目といっしょに、それを保存します。これは、1個のオブジェクト階層をピクルしたり、復元するアプリケーションにとっては問題となりませんが、それ以外の状況では注意を要します。

また、別々にピクルされたオブジェクトでも、それらがすべて同じファイルにピクルされるのであれば、お互いへの参照を維持できるようにする手があるということを指摘しておきたいと思います。pickle モジュールとcPickle モジュールには、すでにピクルされているオブジェクトを追跡し記憶しておくためのPickler (とそれに対応するUnpickler) が用意されています。このPickler を利用することで、共有されている循環参照は、値でピクルされずに、参照でピクルされるようになります (リスト9)。

リスト9. 別々にピクルされたオブジェクト間の参照の維持
>>> 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

ピクルできないオブジェクト

いくつかのオブジェクト型は、ピクルすることができません。たとえば、Pythonは、ファイル・オブジェクト (あるいは、ファイル・オブジェクトへの参照を含むオブジェクト) をピクルすることができません。アンピクルの際にファイルの状態を再現できるかどうかをPythonが保証できないからです(これ以外の例は非常にあいまいなものでこのような性格の記事でとりあげる価値は無いでしょう)。ファイル・オブジェクトをピクルしようとすると、次のようなエラーになります。

リスト10. ファイル・オブジェクトをピクルしようとしたときの結果
>>> 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

クラス・インスタンス

クラス・インスタンスをピクルするときには、単純なオブジェクト型をピクルするときよりも少し注意が必要です。これは、主に、Pythonがインスタンス・データ (通常、_dict_ 属性) とクラスの名前はピクルするが、クラスのコードはピクルしないことに起因しています。Pythonは、クラス・インスタンスをアンピクルする場合、そのインスタンスをピクルしたときに使われたとおりのクラス名とモジュール名 (パッケージのパスを表すプレフィクスも含めて) を使って、そのクラス定義を含んでいるモジュールをインポートしようとします。また、クラス定義は、モジュールのトップ・レベルに示されていなければならない、すなわち、ネストしたクラス (他のクラスや関数の中で定義されたクラス) であってはならない、ということにも注意する必要があります。

クラス・インスタンスがアンピクルされるときには、通常、そのクラスの_init_() メソッドが再度呼び出されることはありません。その代わりに、Pythonは、汎用的なクラス・インスタンスを作成し、ピクルされたインスタンス属性を使い、そのインスタンスの_class_ 属性が元のクラスを指すように設定します。

Python 2.2から導入された新しいスタイルのクラスでは、これとは少し異なるアンピクルのメカニズムが使用されています。基本的に、処理結果は古いスタイルのクラスと同じなのですが、Pythonは、新しいスタイルのクラス・インスタンスを復元するときに、copy_reg モジュールの_reconstructor() 関数を使用します。

新しいスタイルであれ、古いスタイルであれ、クラス・インスタンスに対するデフォルトのピクル動作を変更したい場合には、_getstate_()_setstate_() という名前の特別なクラス・メソッドを定義してやることができます。そうすると、そのクラスのインスタンスの状態データを保存したり復元したりする際に、それらのメソッドがPythonから呼び出されるようになります。後で、これらの特別なメソッドを利用する例をいくつか見てみたいと思います。

ここでは、単純なクラス・インスタンスを見てみることにします。まず、以下のような新しいスタイルのクラス定義を含むpersist.py という名前のPythonモジュールを作成しました。

リスト11. 新しいスタイルのクラス定義
class Foo(object):
    def __init__(self, value):
        self.value = value

そこで、Foo のインスタンスをピクルし、その表現内容 (representation) を調べてみます。

リスト12. 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 と完全修飾モジュール名 (fully qualified module name) のOrbtech.examples.persist の両方が、ピクルに保存されていることがわかります。このインスタンスをファイルにピクルし、後で、あるいは別のマシンでそれをアンピクルしたとすると、Pythonは、Orbtech.examples.persist モジュールをインポートしようとし、インポートできなかったときには例外を出すことになります。クラスやモジュールの名前を変更したり、モジュールを別のディレクトリーに移動すると、やはり同様のエラーが起こることになります。

以下は、クラスFoo の名前を変更した後、先にピクルしておいたFoo のインスタンスをロードしようとした場合に、Pythonが出してくるエラーを示しています。

リスト13. ピクル済みのFooクラスのインスタンスを、クラス名を変更してからロードしようとした場合
>>> 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)。

リスト14. persist.pyモジュールのピクル済みのインスタンスを、モジュール名を変更してからロードしようとした場合
>>> 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

作成済みのピクルに害を及ぼすことなく、この種の変更をうまく処理する技については、後ほどスキーマの進化のところで紹介します。

特別な状態メソッド

先に、ファイル・オブジェクトなど、いくつかのオブジェクト型はピクルできないと述べました。ピクルできないオブジェクトのインスタンス属性を処理する1つの方法は、クラス・インスタンスの状態を変更するための特別なメソッド、_getstate_()_setstate_() を利用することです。以下は、Foo クラスの例で、ファイル・オブジェクトの属性を処理するために手を加えたものです。

リスト15. ピクルできないインスタンスの属性処理
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

Foo のインスタンスをピクルする場合、Pythonは、そのインスタンスの_getstate_() メソッドを呼び出して、そのメソッドから返されてきた値だけをピクルします。同様に、アンピクルを行う場合、Pythonは、インスタンスの_setstate_() メソッドへの引数に、アンピクルされる値を渡します。_setstate_() メソッドの内部では、ピクルした名前や位置のデータを使って、ファイル・オブジェクトを再現し、そのファイル・オブジェクトを、インスタンスのlogfile 属性に割り当てることができます。


スキーマの進化

時間が経過すると、クラス定義に変更を加えなければならなくなります。変更が必要なクラスのインスタンスをすでにピクルしている場合には、新しいクラス定義でも正しく機能し続けるように、それらのインスタンスを読み出してきて更新しなければならないことになるでしょう。われわれは、すでに、クラスやモジュールに変更を加えたときに起こりうるエラーをいくつか見てきました。幸い、ピクル / アンピクル処理には、フックが用意されていて、こうしたスキーマの進化をサポートするために利用することができます。

以下では、よく起こる問題を予測し、それを回避する方法を考えてみたいと思います。クラス・インスタンスのコードはピクルされませんので、メソッドを追加、変更、削除しても、すでにピクル済みのインスタンスに影響を及ぼすことはありません。同じ理由から、クラスの属性についても心配する必要はありません。保証しなければならないのは、クラス定義を含むコード・モジュールをアンピクルの行われる環境に用意しておかなければならないということです。そして、アンピクルの問題を招く可能性のある変更、すなわち、クラス名の変更、インスタンス属性の追加や削除、およびクラス定義モジュールの名前や在処 (ありか) の変更、に備えておく必要があります。

クラス名の変更

以前にピクルされたインスタンスに害を及ぼすことなく、クラスの名前を変更するには、以下の手順に従います。まず、元のクラス定義はそのままにしておき、既存のインスタンスをアンピクルする際にそれを利用できるようにします。元の名前を変更する代わりに、元のクラス定義と同じモジュールにそのクラス定義のコピーを作り、それに新しいクラス名を付けます。そして、元のクラス定義に次のようなメソッドを追加します。NewClassName の部分は、実際には、新しいクラス名とします。

リスト16. クラス名の変更: 元のクラス定義に追加されるメソッド
def __setstate__(self, state):
    self.__dict__.update(state)
    self.__class__ = NewClassName

Pythonは、既存のインスタンスをアンピクルする際、元のクラス定義を見つけ出してくるわけですが、そのとき、インスタンスの_setstate_() メソッドが呼び出され、インスタンスの_class_ 属性が新しいクラス定義に割り当て直されます。既存のインスタンスを、すべて、アンピクルし、更新して、ピクルし直したら、ソース・コード・モジュールから古いクラス定義を削除してかまいません。

属性の追加と削除

これについても、特別な状態メソッド_getstate_()_setstate_() を使って、それぞれのインスタンスの状態を制御したり、インスタンスの属性の変更に対処する機会を得ることができます。簡単なクラス定義を行い、それに属性の追加や削除を行うことにします。以下が、最初の定義です。

リスト17. 最初のクラス定義
class Person(object):
    def __init__(self, firstname, lastname):
        self.firstname = firstname
        self.lastname = lastname

すでにPerson のインスタンスをいろいろと作成し、ピクルしてあるときに、名前の属性を姓名 (first nameとlast name) の2つに分けずに、1つにして保存するようにしたくなったとします。以下は、クラス定義を変更して、すでにピクルされているインスタンスを新しい定義に移行させるための1つのやり方です。

リスト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 を追加し、既存の2つの属性firstnamelastname を削除しています。このとき、ピクル済みのインスタンスをアンピクルすると、以前にピクルされた状態が、辞書として_setstate_() に渡されます。この辞書にfirstnamelastname の2つの属性の値が入っています。そこで、これら2つの値を組み合わせて、それを新しいfullname 属性に割り当てます。その際、状態辞書から古い属性を削除してやります。以前にピクルしたすべてのインスタンスについて更新と再ピクルを済ませたら、クラス定義から_setstate_() メソッドを削除してかまいません。

モジュールの変更

モジュールの名前や在処の変更も、考え方としては、クラス名の変更と同じですが、対処方法は、まったく違ったものとなります。というのも、モジュール情報は、ピクルに保存されるのですが、標準的なピクル・インターフェースによって変更することのできる属性ではないためです。事実、モジュール情報を変更するためには、実際のピクル・ファイル自体に検索・置換操作を行うしかありません。これをどうやって実現するかは、使用しているオペレーティング・システムやツール (適当なもの) によって違ってきます。また、当然ながら、これを行うときには、間違いを犯したときのことを考えて、ファイルをバックアップしておいたほうがよいでしょう。といっても、変更は極めて簡明であり、ピクル・フォーマットがバイナリーであろうがテキストであろうが、問題なく変更できることに変わりはありません。


まとめ

オブジェクトの永続性は、ベースとしているプログラミング言語のオブジェクト・シリアライゼーション機能ごとに異なります。Pythonのオブジェクトの場合は、ピクル処理がこれにあたります。Pythonのピクルは、堅牢で信頼性の高い基盤を用意することで、Pythonのオブジェクトの永続性を効果的に管理できるようにしています。以下の参考文献には、Pythonのピクル処理機能をベースにした各種システムに関する情報を示しておきます。

参考文献

コメント

developerWorks: サイン・イン

必須フィールドは(*)で示されます。


IBM ID が必要ですか?
IBM IDをお忘れですか?


パスワードをお忘れですか?
パスワードの変更

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


お客様が developerWorks に初めてサインインすると、お客様のプロフィールが作成されます。会社名を非表示とする選択を行わない限り、プロフィール内の情報(名前、国/地域や会社名)は公開され、投稿するコンテンツと一緒に表示されますが、いつでもこれらの情報を更新できます。

送信されたすべての情報は安全です。

ディスプレイ・ネームを選択してください



developerWorks に初めてサインインするとプロフィールが作成されますので、その際にディスプレイ・ネームを選択する必要があります。ディスプレイ・ネームは、お客様が developerWorks に投稿するコンテンツと一緒に表示されます。

ディスプレイ・ネームは、3文字から31文字の範囲で指定し、かつ developerWorks コミュニティーでユニークである必要があります。また、プライバシー上の理由でお客様の電子メール・アドレスは使用しないでください。

必須フィールドは(*)で示されます。

3文字から31文字の範囲で指定し

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


送信されたすべての情報は安全です。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=Linux
ArticleID=288895
ArticleTitle=Pythonでの永続性管理
publish-date=11012002