Boost.Python における pickle のサポート

pickle はオブジェクトの直列化(または永続化、整列化、平坦化)のための Python モジュールである。

オブジェクトの内容をファイルに保存、またはファイルから復元する必要があることはよくある。解法の 1 つは、特殊な形式でファイルへデータを読み書きする関数の組を書くことである。他の強力な解法は Python の pickle モジュールを使うことである。Python の自己記述機能を利用すると、pickle モジュールはほとんど任意の Python オブジェクトを再帰的にファイルに書き込み可能なバイトストリームへ変換する。

Boost.Python ライブラリは、Python ライブラリリファレンスの pickle の項に詳細記載のインターフェイスを通じて pickle モジュールをサポートする。このインターフェイスは以下に述べる特殊メソッド __getinitargs____getstate__ および __setstate__ を必要とする。Boost.Python は Python の cPickle モジュールとも完全に互換であることに注意していただきたい。

Boost.Python の pickle インターフェイス

ユーザレベルでは、Boost.Python の pickle インターフェイスは 3 つの特殊メソッドを伴う。

__getinitargs__

Boost.Python 拡張クラスのインスタンスを pickle 化するとき、pickler はインスタンスが __getinitargs__ メソッドを持っているかテストする。このメソッドは Python のタプルを返さなければならない(boost::python::tuple を使うのが最も便利である)。インスタンスを unpickler が復元するとき、このタプルの内容をクラスのコンストラクタの引数として使用する。

__getinitargs__ が定義されていない場合、pickle.load は引数無しでコンストラクタ(__init__)を呼び出す。すなわちオブジェクトはデフォルトコンストラクト可能でなければならない。

__getstate__

Boost.Python 拡張クラスのインスタンスを pickle 化するとき、pickler はインスタンスが __getstate__ メソッドを持っているかテストする。このメソッドはインスタンスの状態を表す Python オブジェクトを返さなければならない。

__setstate__

Boost.Python 拡張クラスのインスタンスを unpickler により復元(pickle.load)するとき、はじめに __getinitargs__ の結果を引数として構築する(上述)。次に unpickler は新しいインスタンスが __setstate__ メソッドを持っているかテストする。テストが成功した場合、__getstate__ の結果(Python オブジェクト)を引数としてこのメソッドを呼び出す。

上記 3 つの特殊メソッドは、ユーザが個別に .def してもよい。しかしながら Boost.Python は簡単に使用できる高水準インターフェイスを boost::python::pickle_suite クラスで提供している。このクラスは、__getstate__ および __setstate__ を組として定義しなければならないという一貫性も強制する。このインターフェイスの使用方法は以下の例で説明する。

boost/libs/python/test に、pickle サポートを提供する方法を示したファイルが 3 つある。

pickle1.cpp 1

この例の C++ クラスは、コンストラクタに適切な引数を渡すことで完全に復元できる。よって pickle インターフェイスのメソッド __getinitargs__ を定義するのに十分である。以下のようにする。

  1. C++ の pickle 関数の定義:

    struct world_pickle_suite : boost::python::pickle_suite
    {
      static
      boost::python::tuple
      getinitargs(world const& w)
      {
          return boost::python::make_tuple(w.get_country());
      }
    };
    
  2. Python の束縛を確立する。

    class_<world>("world", args<const std::string&>())
        // ...
        .def_pickle(world_pickle_suite())
        // ...
    

pickle2.cpp 2

この例の C++ クラスは、コンストラクタで復元不可能なメンバデータを持つ。よって pickle インターフェイスのメソッド組 __getstate____setstate__ を提供する必要がある。

  1. C++ の pickle 関数の定義:

    struct world_pickle_suite : boost::python::pickle_suite
    {
      static
      boost::python::tuple
      getinitargs(const world& w)
      {
        // ...
      }
    
      static
      boost::python::tuple
      getstate(const world& w)
      {
        // ...
      }
    
      static
      void
      setstate(world& w, boost::python::tuple state)
      {
        // ...
      }
    };
    
  2. suite 全体の Python の束縛を確立する。

    class_<world>("world", args<const std::string&>())
        // ...
        .def_pickle(world_pickle_suite())
        // ...
    

簡単のために、__getstate__ の結果に __dict__ は含まれない。これは通常は推奨しないが、オブジェクトの __dict__ が常に空であると分かっている場合は有効な方法である。この想定が崩れるケースは以下に述べる安全柵で捕捉できる。

pickle3.cpp 3

この例は pickle2.cpp と似ているが、__getstate__ の結果にオブジェクトの __dict__ が含まれる。より多くのコードが必要になるが、オブジェクトの __dict__ が空とは限らない場合は避けられない。

落とし穴と安全柵

上述の pickle プロトコルには、Boost.Python 拡張モジュールのエンドユーザが気にかけない重大な落とし穴がある。

重要

__getstate__ が定義されており、インスタンスの __dict__ が空でない。

Boost.Python 拡張クラスの作成者は、以下の可能性を考慮せずに __getstate__ を提供する可能性がある。

  • クラスが Python 内で基底クラスとして使用される。おそらく派生クラスのインスタンスの __dict__ は、インスタンスを正しく復元するために pickle 化する必要がある。

  • ユーザがインスタンスの __dict__ に直接要素を追加する。この場合もインスタンスの __dict__ は pickle 化が必要である。

この高度に不明確な問題をユーザに警告するために、安全柵が提供されている。__getstate__ が定義されており、インスタンスの __dict__ が空でない場合は、Boost.Python はクラスが属性 __getstate_manages_dict__ を持っているかテストする。この属性が定義されていなければ例外を送出する。

RuntimeError: Incomplete pickle support (__getstate_manages_dict__ not set)

この問題を解決するには、まず __getstate__ および __setstate__ メソッドがインスタンスの __dict__ を正しく管理するようにしなければならない。これは C++ あるいは Python レベルのいずれでも達成可能であることに注意していただきたい。最後に安全柵を故意にオーバーライドしなければならない。例えば C++ では以下のとおり(pickle3.cpp から抜粋)。

struct world_pickle_suite : boost::python::pickle_suite
{
  // ...

  static bool getstate_manages_dict() { return true; }
};

あるいは Python では次のとおり。

import your_bpl_module
class your_class(your_bpl_module.your_class):
  __getstate_manages_dict__ = 1
  def __getstate__(self):
    # ここにコードを書く
  def __setstate__(self, state):
    # ここにコードを書く

実践的なアドバイス

  1. 多くの拡張クラスを持つ Boost.Python 拡張モジュールでは、すべてのクラスについて pickle の完全なサポートを提供すると著しいオーバーヘッドとなる。通常、完全な pickle サポートの実装は最終的に pickle 化する拡張クラスに限定すべきである。

  2. インスタンスが __getinitargs__ による再構築も可能な場合は __getstate__ は避けよ。これは上記の落とし穴を自動的に避けることになる。

  3. __getstate__ が必要な場合、返す Python オブジェクトにインスタンスの __dict__ を含めよ。

軽量な代替:Python 側での pickle サポートの実装

pickle4.cpp 4

pickle4.cpp の例は、pickle サポートの実装に関する別のテクニックのデモンストレーションである。はじめに class_::enable_pickling メンバ関数で pickle 化に必要な基本的な属性だけを Boost.Python に定義させる。

class_<world>("world", args<const std::string&>())
    // ...
    .enable_pickling()
    // ...

これで Python のドキュメントにある標準的な Python の pickle インターフェイスが有効になる。__getinitargs__ メソッドをラップするクラス定義に「注入」することで、すべてのインスタンスを pickle 化可能にする。

# ラップした world クラスをインポート
from pickle4_ext import world

# __getinitargs__ の定義
def world_getinitargs(self):
  return (self.get_country(),)

# ここで __getinitargs__ を注入(Python は動的言語!)
world.__getinitargs__ = world_getinitargs

Python から追加のメソッドを注入する方法については、チュートリアルの節も見よ。

1

http://www.boost.org/libs/python/test/pickle1.cpp

2

http://www.boost.org/libs/python/test/pickle2.cpp

3

http://www.boost.org/libs/python/test/pickle3.cpp

4

http://www.boost.org/libs/python/test/pickle4.cpp