
Python 3: Deep Dive (Part 3 - Dictionaries, Sets, JSON): 演習② (セクション8/12)
Pythonの`json`モジュールで日付やDecimalなど複雑な型をシリアライズ・デシリアライズするにはカスタム処理が必要。
Marshmallowを使うと、スキーマ定義によりカスタムクラスや型情報を保持したまま簡易にJSONとの変換が可能。
結果的に、コード量と複雑さを減らしつつ、信頼性の高いデータ変換が実現できる。
イントロダクション
『Python 3: Deep Dive (Part 3 - Dictionaries, Sets, JSON)』のセクション8では、理論的概念から一歩踏み出し、実際のコーディング演習へと移行します。この章では、Python の組み込み `json` ライブラリおよび強力なサードパーティライブラリである Marshmallow を用いて、シリアライズとデシリアライズに焦点を当てます。ここでは、複雑なカスタムオブジェクトを JSON 文字列へ、そして再び失われたくない型情報を保持したまま戻す方法を学びます。
日付や時間、decimal、カスタムクラスなどを扱う場合、適切なシリアライズとデシリアライズを理解することは不可欠です。本稿では、手動での `json` ベースのアプローチから、Marshmallow を使ったより洗練されたアプローチまで、演習とその解法を丁寧に解説します。
背景設定:カスタムクラスと複雑なデータ
はじめに扱うのは `Stock` クラスと `Trade` クラスという2つのカスタムクラスです。それぞれ、以下のような属性を持ちます。
Stock: シンボル(文字列)、日付(`datetime.date`)、`Decimal` 型のオープン/ハイ/ロー/クローズ価格、そして出来高(整数)。
Trade: シンボル(文字列)、タイムスタンプ(`datetime.datetime`)、注文種別(文字列)、価格(`Decimal`)、出来高(整数)、手数料(`Decimal`)。
これらは現実世界の金融データを模したものです。課題は、これらを JSON へとシリアライズし、後に元の Python オブジェクトに戻せるようにすること。その際、日付や時刻、decimal やカスタムクラスといったデータ型を失わずに再現する必要があります。
演習1:カスタム JSON エンコード
最初の演習では、これらの `Stock` や `Trade` インスタンスを含む辞書をシリアライズするためのカスタム JSON エンコーダーを書くことが課題です。単純に `json.dumps()` を使っても、JSON は `date`、`datetime`、`Decimal` オブジェクトを直接扱えないので失敗します。解決策として、以下を実現しなければなりません。
`Stock` および `Trade` オブジェクトを判別し、特別な識別子(例:"object": "Stock"、"object": "Trade")を JSON 内に埋め込む。
`date` および `datetime` オブジェクトを ISO 形式の文字列に変換する。
`Decimal` オブジェクトを文字列に変換して精度を失わないようにする。
完全に JSON シリアライズ可能な構造を返す。
ポイント:
`JSONEncoder` を継承したカスタムクラスを実装する。
`default` メソッド内で `datetime`、`date`、`Decimal`、`Stock`、`Trade` を順に処理する。
JSON が理解できる形式(文字列など)に変換する。
これらを行うと、`json.dumps(activity, cls=CustomJSONEncoder)` で整形された JSON が得られ、`"object": "Stock"` や `"object": "Trade"` といったフィールドが含まれます。
結果例:
{
"quotes": [
{
"symbol": "TSLA",
"date": "2018-11-22",
"open": "338.19",
"high": "338.64",
"low": "337.60",
"close": "338.19",
"volume": 365607,
"object": "Stock"
},
...
],
"trades": [
{
"symbol": "TSLA",
"timestamp": "2018-11-22T10:05:12",
"order": "buy",
"price": "338.25",
"volume": 100,
"commission": "9.99",
"object": "Trade"
},
...
]
}
演習2:カスタム JSON デコード
シリアライズができたら、次は逆方向です。今度は JSON 文字列を読み込み、元の Python オブジェクトへ戻します。これには以下が必要です。
JSON 文字列を Python の辞書へ戻す。
`"object": "Stock"` や `"object": "Trade"` といった特別な識別子を見て、適切なクラスインスタンスを再構築する。
ISO形式の日付・時刻文字列を `date`、`datetime` オブジェクトに戻す。
`Decimal` を表す文字列を再び `Decimal` オブジェクトへ戻す。
アプローチ:
`object_hook` 関数または `JSONDecoder` のサブクラスで各辞書を検査する。
`"object": "Stock"` なら `Stock` オブジェクトを生成。
`"object": "Trade"` なら `Trade` オブジェクトを生成。
日付文字列は `datetime` モジュール、`Decimal` 文字列は `Decimal` で復元。
検証:
`Stock` と `Trade` に `eq` メソッドを定義して、元の `activity` 構造とデシリアライズ後の構造を比較するとよいです。
decoded_activity = json.loads(encoded_activity_json, cls=CustomJSONDecoder)
assert decoded_activity == activity # True であることを確認
演習3:Marshmallow の活用
ここまでで `json` モジュールによる手動のシリアライズ・デシリアライズの大変さを実感したことでしょう。ここで Marshmallow を使うと、驚くほど簡潔で強力な方法が得られます。Marshmallow では、スキーマを定義して、複雑なオブジェクトと Python ネイティブデータ型をマッピングし、ネスト構造や検証も容易になります。
Marshmallow を使う場合:
`StockSchema` と `TradeSchema` を定義し、`fields.Str()`、`fields.Date()`、`fields.Decimal(as_string=True)` などでフィールドを指定。
`@post_load` メソッドで辞書から `Stock` や `Trade` オブジェクトを直接生成。
`schema.dumps()` でシリアライズ、`schema.loads()` でデシリアライズ可能。
結果:
Marshmallow は多くの定型処理を肩代わりします。例えば:
class StockSchema(Schema):
symbol = fields.Str()
date = fields.Date()
open = fields.Decimal(as_string=True)
high = fields.Decimal(as_string=True)
low = fields.Decimal(as_string=True)
close = fields.Decimal(as_string=True)
volume = fields.Int()
@post_load
def make_stock(self, data, **kwargs):
data['open_'] = data.pop('open')
return Stock(**data)
`ActivitySchema` で `Nested` を使って quotes や trades リストを定義すれば、シリアライズ・デシリアライズはさらに簡潔になります。Marshmallow は自動的に JSON 対応形式と Python オブジェクトを相互変換し、コード量と複雑さを大幅に軽減します。
まとめ:
手動 vs. 自動: 標準の `json` モジュールで全てを手動で行うと、複雑な型対応が大変です。Marshmallow はこうしたタスクを大幅に簡略化します。
型情報の保持: 適切なスキーマやインジケータを用いることで、日付・時刻・decimal・カスタムクラスなどの型情報を失わずにシリアライズ/デシリアライズできます。
テストと検証: シリアライズ・デシリアライズ処理後に元データと比較して検証することで、処理が正しく機能していることを確認できます。
柔軟性: `json` モジュールは細かな制御が可能ですが記述量が増えます。Marshmallow は使いやすさと柔軟性のバランスが取れています。
結論:
セクション8のコーディング演習では、`json` モジュールを用いたローレベルなシリアライズ・デシリアライズから、Marshmallow を使ったエレガントな方法までを経験します。日付、時刻、decimal、カスタムクラスの扱い方を学び、型情報を正しく保持しつつ JSON と Python オブジェクトを自由に行き来できるようになります。
最終的には、複雑な Python オブジェクトを JSON へ、そして再び元に戻し、その過程で構造と意味を失わない技術を習得できます。これは JSON のようなデータ交換フォーマットが広く使われる現代のソフトウェア開発では極めて有用であり、信頼性とメンテナンス性の向上にも大いに寄与します。