あなたの質問へのコメントで述べたように、json
モジュールのソースコードを見た後、あなたが望むことをするのに向いていないようです。しかし、この目標は、モンキー パッチと呼ばれるものによって達成できます(モンキーパッチとは? の
質問を参照してください)。これはパッケージの初期化スクリプトで行うことができ、モジュールは通常 1 回だけロードされ、結果は にキャッシュされるため、__init__.py
後続のすべてのjson
モジュールのシリアル化に影響しsys.modules
ます。
このパッチは、デフォルトの json エンコーダーのdefault
メソッド (デフォルトのdefault()
.
簡単にするために、スタンドアロン モジュールとして実装された例を次に示します。
モジュール:make_json_serializable.py
""" Module that monkey-patches json module when it's imported so
JSONEncoder.default() automatically checks for a special "to_json()"
method and uses it to encode the object if found.
"""
from json import JSONEncoder
def _default(self, obj):
return getattr(obj.__class__, "to_json", _default.default)(obj)
_default.default = JSONEncoder.default # Save unmodified default.
JSONEncoder.default = _default # Replace it.
モジュールをインポートするだけでパッチが適用されるため、使い方は簡単です。
サンプル クライアント スクリプト:
import json
import make_json_serializable # apply monkey-patch
class Foo(object):
def __init__(self, name):
self.name = name
def to_json(self): # New special method.
""" Convert to JSON format string representation. """
return '{"name": "%s"}' % self.name
foo = Foo('sazpaz')
print(json.dumps(foo)) # -> "{\"name\": \"sazpaz\"}"
オブジェクト タイプ情報を保持するために、特別なメソッドは返される文字列にそれを含めることもできます。
return ('{"type": "%s", "name": "%s"}' %
(self.__class__.__name__, self.name))
これにより、クラス名が含まれるようになった次の JSON が生成されます。
"{\"type\": \"Foo\", \"name\": \"sazpaz\"}"
魔法はここにあります
特別な名前のメソッドを置き換えるよりもさらに良いのは、特別なメソッドを追加する必要なく、ユーザー定義のクラス インスタンスを含むほとんどの Python オブジェクトを自動的default()
にシリアライズできることです。いくつかの代替案を調査した後、モジュールを使用する @Raymond Hettinger による別の質問への回答に基づいて、次のものがその理想に最も近いように思えました。pickle
モジュール:make_json_serializable2.py
""" Module that imports the json module and monkey-patches it so
JSONEncoder.default() automatically pickles any Python objects
encountered that aren't standard JSON data types.
"""
from json import JSONEncoder
import pickle
def _default(self, obj):
return {'_python_object': pickle.dumps(obj)}
JSONEncoder.default = _default # Replace with the above.
もちろん、すべてをピクルすることはできません。たとえば、拡張タイプです。ただし、特別なメソッドを作成することにより、pickle プロトコルを介してそれらを処理する方法が定義されています。
逆シリアル化
それにもかかわらず、pickle プロトコルを使用すると、渡されたディクショナリ内の任意のキーを使用する任意の呼び出しにカスタムobject_hook
関数引数を提供することで、元の Python オブジェクトを再構築することがかなり簡単になることも意味します。何かのようなもの:json.loads()
'_python_object'
def as_python_object(dct):
try:
return pickle.loads(str(dct['_python_object']))
except KeyError:
return dct
pyobj = json.loads(json_str, object_hook=as_python_object)
これを多くの場所で行う必要がある場合は、追加のキーワード引数を自動的に提供するラッパー関数を定義する価値があります。
json_pkloads = functools.partial(json.loads, object_hook=as_python_object)
pyobj = json_pkloads(json_str)
当然、これはjson
モジュールにモンキー パッチを適用して、関数をobject_hook
(の代わりにNone
) デフォルトにすることもできます。
Raymond Hettingerによる別の JSON シリアライゼーションの質問への回答pickle
から使用するアイデアを得ました。これは、非常に信頼できるものであり、公式のソース (Python コア開発者のように) と考えています。
Python 3 への移植性
が処理できないオブジェクトをjson.dumps()
返すため、上記のコードは Python 3 で示されているようには機能しません。ただし、アプローチはまだ有効です。この問題を回避する簡単な方法は、返された値を「デコード」してから「エンコード」してから関数に渡すことです。これは、任意のバイナリ文字列が有効であり、常に Unicode にデコードしてから元の文字列に再度エンコードできるためです (この回答でSven Marnachが指摘したように)。bytes
JSONEncoder
latin1
pickle.dumps()
latin1
pickle.loads()
as_python_object()
latin1
(以下は Python 2 で正常に動作しますが、latin1
デコードとエンコードは不要です。)
from decimal import Decimal
class PythonObjectEncoder(json.JSONEncoder):
def default(self, obj):
return {'_python_object': pickle.dumps(obj).decode('latin1')}
def as_python_object(dct):
try:
return pickle.loads(dct['_python_object'].encode('latin1'))
except KeyError:
return dct
class Foo(object): # Some user-defined class.
def __init__(self, name):
self.name = name
def __eq__(self, other):
if type(other) is type(self): # Instances of same class?
return self.name == other.name
return NotImplemented
__hash__ = None
data = [1,2,3, set(['knights', 'who', 'say', 'ni']), {'key':'value'},
Foo('Bar'), Decimal('3.141592653589793238462643383279502884197169')]
j = json.dumps(data, cls=PythonObjectEncoder, indent=4)
data2 = json.loads(j, object_hook=as_python_object)
assert data == data2 # both should be same