pickleモジュールのプロトコルは、__init__が副作用や高コストの計算を持つオブジェクトを処理するために進化しました。初期のプロトコルは、デシリアライズ中に__init__を呼び出す必要があり、ファイルハンドルやデータベース接続のようなリソースに問題を引き起こしました。プロトコル2は__getnewargs__を導入し、プロトコル4は__getnewargs_ex__を拡張してキーワード引数をサポートし、オブジェクト再構築に対するより詳細な制御を提供します。
オブジェクトをデシリアライズするとき、Pythonは通常、オブジェクトの状態を再作成する必要があります。もし__init__が検証を行ったり、ネットワークソケットを開いたり、グローバルな状態を変更したりする場合、デシリアライズ中にこれを再実行することは不正確または非効率的です。課題は、これらの初期化の副作用を引き起こさずにオブジェクトの状態を復元することです。ストレージされたデータのみを使用して、低レベルの__new__コンストラクタを通じてインスタンスを再構築します。
__getnewargs_ex__ダンダーメソッド(または古いプロトコルの場合の__getnewargs__)は、クラスが__new__に直接渡される(args, kwargs)のタプルを返すことを可能にし、__init__を完全にスキップします。このメソッドは再構築フェーズ中に呼び出され、その戻り値がシリアライズされたバイトからインスタンスがどのように作成されるかを決定します。このアプローチは、復元されたオブジェクトに不適切な初期化ロジックを呼び出さずに、正しい初期状態でオブジェクトをインスタンス化することを保証します。
import pickle class DatabaseConnection: def __new__(cls, dsn, timeout=30): instance = super().__new__(cls) instance.dsn = dsn instance.timeout = timeout return instance def __init__(self, dsn, timeout=30): # デシリアライズ中にスキップしたい高コストの操作 self.socket = create_socket(dsn, timeout) def __getnewargs_ex__(self): # __new__のための引数とキーワード引数を返す return ((self.dsn,), {'timeout': self.timeout}) def __getstate__(self): # ソケットをピクルしない return {'dsn': self.dsn, 'timeout': self.timeout} def __setstate__(self, state): self.dsn = state['dsn'] self.timeout = state['timeout'] # 必要に応じてソケットを再確立するか、遅延初期化のためにそのままにする # 使用例 conn = DatabaseConnection('postgresql://localhost', timeout=60) serialized = pickle.dumps(conn, protocol=4) restored = pickle.loads(serialized) # __init__は呼ばれない
データ処理パイプラインは、オープンなTCPソケットと認証トークンを保持するRedis接続オブジェクトをキャッシュします。これらのキャッシュエントリを永続化のためにディスクにシリアライズする際、デシリアライズ中に__init__を呼び出すと、新しいソケット接続をすぐに作成しようとし、オフライン環境では失敗したりリソースリークを引き起こします。このシナリオでは、接続パラメータを保持しつつ、アプリケーションが明示的に要求するまで実際のネットワーク確立を遅らせるシリアル化戦略が必要です。
__getstate__を実装して接続パラメータ(ホスト、ポート、認証)だけを返し、__setstate__を実装して属性を手動で設定し、オプションで接続を再オープンします。このアプローチは古いpickleプロトコルとも互換性があり、明示的です。ただし、__reduce__を使って注意深く回避しない限り、デフォルトのデシリアライズプロセス中に__init__を呼び出すため、副作用が__setstate__がクリーンアップできる前にトリガーされる可能性があります。
__reduce__を実装して(callable, args, state)のタプルを返すようにします。ここで、callableはクラスメソッドまたは__new__自体です。これにより、再構築の完全な制御が提供されますが、冗長で状態辞書の手動管理が必要になります。これにより、コードの複雑さが増し、クラス構造とピクルデータとの間にバージョン不一致のリスクが高まります。
__getnewargs_ex__を実装して((host, port), {'auth': token})を返し、pickleが__new__(host, port, auth=token)を直接呼び出せるようにします。これにより、__init__をスキップしながらインスタンスを作成することができます。このソリューションは、現代のプロトコル4の機能を活用し、'空のインスタンス作成'フェーズと'リソースの初期化'フェーズを明確に分離し、__reduce__のボイラープレートを避けるために選択されました。その結果、接続オブジェクトが構成を保持したまま復元される堅牢なキャッシングシステムが実現され、ソケットは明示的に必要になるまで閉じたままで、バッチデシリアライズ操作中にリソースの枯渇を防ぎます。
なぜ__getnewargs_ex__は__init__が呼び出されないことを防ぎ、単独の__setstate__はそうしないのか?
pickleがオブジェクトを再構築するとき、それは__getnewargs_ex__(または__getnewargs__)の存在をチェックします。存在する場合、デシリアライザは返された値で__new__(*args, **kwargs)を呼び出し、__setstate__が利用可能であればすぐにその状態を適用し、__init__を完全にスキップします。対照的に、これらのメソッドがない場合、pickleはデフォルトの構築パスを使用し、__new__の後に常に__init__を呼び出します。候補者はしばしば__setstate__が初期化をオーバーライドすると思いがちですが、__setstate__は単に__init__が既に実行された後にインスタンスを修正するだけであり、副作用防止には遅すぎるのです。
__getnewargs_ex__が2要素のタプルでない値を返したらどうなりますか?
pickleプロトコルは厳格に__getnewargs_ex__が長さ2のタプル(args_tuple, kwargs_dict)を返す必要があります。もしシングルタプルの引数(__getnewargs__のように)を返す場合、Pythonはデシリアライズ中にTypeErrorを発生させます。これはその結果を__new__(*args, **kwargs)にアンパックしようとするからです。もしNoneや他のタイプを返すと、デシリアライザはクラッシュするか、予測不可能な動作をする可能性があり、__getnewargs__は引数のタプルだけを期待します。
__getnewargs_ex__は__reduce_ex__とどのように相互作用しますか?両方が定義されている場合は?
__reduce_ex__はシリアル化を調整する高水準のプロトコルメソッドです。クラスが__getnewargs_ex__を定義している場合、__reduce_ex__(特にプロトコル4+では)はその戻り値をNEWOBJ_EXオペコードを使用して減少タプルに自動的に組み込みます。両方が存在し、__reduce_ex__が標準の再構築パスを使用しないカスタムcallableを返すと、それが優先され、__getnewargs_ex__が完全に無視される可能性があります。