作为对您问题的评论,我已查看了
json
模块的源代码,并不适用于您想要做的事情。然而,通过所谓的
monkey-patching可以实现目标(请参见问题
什么是monkey patch?)。这可以在您包的
__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
JSONEncoder.default = _default
只需要导入该模块,就可以轻松地使用它。
示例客户端脚本:
import json
import make_json_serializable
class Foo(object):
def __init__(self, name):
self.name = name
def to_json(self):
""" Convert to JSON format string representation. """
return '{"name": "%s"}' % self.name
foo = Foo('sazpaz')
print(json.dumps(foo))
为了保留对象类型信息,特殊方法也可以将其包含在返回的字符串中:
return ('{"type": "%s", "name": "%s"}' %
(self.__class__.__name__, self.name))
这将生成以下JSON,其中现在包括类名:
"{\"type\": \"Foo\", \"name\": \"sazpaz\"}"
魔法就在这里
比让替换的default()
方法查找特定命名方法更好的是,它应该能够自动序列化大多数Python对象,包括用户定义的类实例,而不需要添加特殊方法。经过研究多种替代方案后,以下方法——基于@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
当然,不能将所有东西都进行pickle处理,例如扩展类型。但是,通过编写特殊方法(类似于您建议和我之前描述的内容),可以定义处理它们的方式,并使用pickle协议。不过,这样做可能只需要针对更少的情况。
反序列化
无论如何,使用pickle协议也意味着在任何json.loads()调用中提供自定义object_hook函数参数,以便在传入字典中使用任何'_python_object'键时(如果有的话),可以相当容易地重构原始Python对象。例如:
```python
json.loads(json_string, object_hook=python_object_decoder)
```
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序列化问题的
answer中得到了使用
pickle
的想法,我认为他非常可信,也是官方来源(即Python核心开发人员)。
适用于Python 3的可移植性
上面的代码在Python 3中不能像所示那样工作,因为
json.dumps()
返回一个
bytes
对象,而
JSONEncoder
无法处理。但是这种方法仍然有效。解决此问题的简单方法是对从
pickle.dumps()
返回的值进行
latin1
“解码”,然后从
latin1
“编码”再传递给
pickle.loads()
在
as_python_object()
函数中。这是有效的,因为任意二进制字符串都是有效的
latin1
,可以始终将其解码为Unicode,然后再次编码为原始字符串(如
this answer和
Sven Marnach所指出的)。
(尽管以下内容在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):
def __init__(self, name):
self.name = name
def __eq__(self, other):
if type(other) is type(self):
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
json
模块的encoder.py
文件中没有看到类似的内容。 - martineau__json__
的东西还没有得到支持的人,可以参考以下讨论:cpython issue #79292,cpython issue #71549。 - undefined