如何使一个Python类序列化?
class FileItem:
def __init__(self, fname):
self.fname = fname
尝试序列化为JSON:
>>> import json
>>> x = FileItem('/foo/bar')
>>> json.dumps(x)
TypeError: Object of type 'FileItem' is not JSON serializable
如何使一个Python类序列化?
class FileItem:
def __init__(self, fname):
self.fname = fname
尝试序列化为JSON:
>>> import json
>>> x = FileItem('/foo/bar')
>>> json.dumps(x)
TypeError: Object of type 'FileItem' is not JSON serializable
当前回答
我们经常在日志文件中转储JSON格式的复杂字典。虽然大多数字段携带重要信息,但我们不太关心内置的类对象(例如子进程)。Popen对象)。由于存在这些不可序列化的对象,对json.dumps()的调用会失败。
为了解决这个问题,我构建了一个小函数来转储对象的字符串表示形式,而不是转储对象本身。如果您正在处理的数据结构嵌套太多,您可以指定嵌套的最大级别/深度。
from time import time
def safe_serialize(obj , max_depth = 2):
max_level = max_depth
def _safe_serialize(obj , current_level = 0):
nonlocal max_level
# If it is a list
if isinstance(obj , list):
if current_level >= max_level:
return "[...]"
result = list()
for element in obj:
result.append(_safe_serialize(element , current_level + 1))
return result
# If it is a dict
elif isinstance(obj , dict):
if current_level >= max_level:
return "{...}"
result = dict()
for key , value in obj.items():
result[f"{_safe_serialize(key , current_level + 1)}"] = _safe_serialize(value , current_level + 1)
return result
# If it is an object of builtin class
elif hasattr(obj , "__dict__"):
if hasattr(obj , "__repr__"):
result = f"{obj.__repr__()}_{int(time())}"
else:
try:
result = f"{obj.__class__.__name__}_object_{int(time())}"
except:
result = f"object_{int(time())}"
return result
# If it is anything else
else:
return obj
return _safe_serialize(obj)
由于字典也可以有不可序列化的键,转储它们的类名或对象表示将导致所有键都具有相同的名称,这将抛出错误,因为所有键都需要有唯一的名称,这就是为什么当前时间Since epoch被int(time())附加到对象名称。
可以使用以下具有不同级别/深度的嵌套字典来测试该函数
d = {
"a" : {
"a1" : {
"a11" : {
"a111" : "some_value" ,
"a112" : "some_value" ,
} ,
"a12" : {
"a121" : "some_value" ,
"a122" : "some_value" ,
} ,
} ,
"a2" : {
"a21" : {
"a211" : "some_value" ,
"a212" : "some_value" ,
} ,
"a22" : {
"a221" : "some_value" ,
"a222" : "some_value" ,
} ,
} ,
} ,
"b" : {
"b1" : {
"b11" : {
"b111" : "some_value" ,
"b112" : "some_value" ,
} ,
"b12" : {
"b121" : "some_value" ,
"b122" : "some_value" ,
} ,
} ,
"b2" : {
"b21" : {
"b211" : "some_value" ,
"b212" : "some_value" ,
} ,
"b22" : {
"b221" : "some_value" ,
"b222" : "some_value" ,
} ,
} ,
} ,
"c" : subprocess.Popen("ls -l".split() , stdout = subprocess.PIPE , stderr = subprocess.PIPE) ,
}
执行以下命令将会得到-
print("LEVEL 3")
print(json.dumps(safe_serialize(d , 3) , indent = 4))
print("\n\n\nLEVEL 2")
print(json.dumps(safe_serialize(d , 2) , indent = 4))
print("\n\n\nLEVEL 1")
print(json.dumps(safe_serialize(d , 1) , indent = 4))
结果:
LEVEL 3
{
"a": {
"a1": {
"a11": "{...}",
"a12": "{...}"
},
"a2": {
"a21": "{...}",
"a22": "{...}"
}
},
"b": {
"b1": {
"b11": "{...}",
"b12": "{...}"
},
"b2": {
"b21": "{...}",
"b22": "{...}"
}
},
"c": "<Popen: returncode: None args: ['ls', '-l']>"
}
LEVEL 2
{
"a": {
"a1": "{...}",
"a2": "{...}"
},
"b": {
"b1": "{...}",
"b2": "{...}"
},
"c": "<Popen: returncode: None args: ['ls', '-l']>"
}
LEVEL 1
{
"a": "{...}",
"b": "{...}",
"c": "<Popen: returncode: None args: ['ls', '-l']>"
}
[注意]:仅在不关心内置类对象的序列化时使用此选项。
其他回答
正如在许多其他答案中提到的,您可以将函数传递给json。转储将不是默认支持的类型之一的对象转换为受支持的类型。令人惊讶的是,他们都没有提到最简单的情况,即使用内置函数vars将对象转换为包含其所有属性的dict:
json.dumps(obj, default=vars)
注意,这只涵盖了基本的情况,如果你需要对某些类型进行更具体的序列化(例如排除某些属性或没有__dict__属性的对象),你需要使用自定义函数或JSONEncoder,如其他答案中所述。
只需要像这样添加to_json方法到你的类中:
def to_json(self):
return self.message # or how you want it to be serialized
然后将这段代码(来自这个答案)添加到所有内容的顶部:
from json import JSONEncoder
def _default(self, obj):
return getattr(obj.__class__, "to_json", _default.default)(obj)
_default.default = JSONEncoder().default
JSONEncoder.default = _default
这将会在导入json模块时monkey-patch,所以 JSONEncoder.default()自动检查特殊的to_json() 方法,并使用它对找到的对象进行编码。
就像Onur说的,但是这次你不需要更新项目中的每个json.dumps()。
你知道预期产量是多少吗?例如,这个可以吗?
>>> f = FileItem("/foo/bar")
>>> magic(f)
'{"fname": "/foo/bar"}'
在这种情况下,你只需调用json.dumps(f.__dict__)。
如果您想要更多自定义输出,那么您必须继承JSONEncoder并实现您自己的自定义序列化。
对于一个简单的例子,请参见下面。
>>> from json import JSONEncoder
>>> class MyEncoder(JSONEncoder):
def default(self, o):
return o.__dict__
>>> MyEncoder().encode(f)
'{"fname": "/foo/bar"}'
然后你把这个类作为cls kwarg传递给json.dumps()方法:
json.dumps(cls=MyEncoder)
如果还想解码,则必须向JSONDecoder类提供一个自定义object_hook。例如:
>>> def from_json(json_object):
if 'fname' in json_object:
return FileItem(json_object['fname'])
>>> f = JSONDecoder(object_hook = from_json).decode('{"fname": "/foo/bar"}')
>>> f
<__main__.FileItem object at 0x9337fac>
>>>
如果你不介意为它安装一个包,你可以使用json-tricks:
pip install json-tricks
之后,你只需要从json_tricks导入dump(s)而不是json,它通常会工作:
from json_tricks import dumps
json_str = dumps(cls_instance, indent=4)
这将给
{
"__instance_type__": [
"module_name.test_class",
"MyTestCls"
],
"attributes": {
"attr": "val",
"dct_attr": {
"hello": 42
}
}
}
基本上就是这样!
这在一般情况下会很有效。有一些例外,例如,如果特殊的事情发生在__new__中,或者更多的元类魔法正在发生。
显然加载也可以(否则有什么意义):
from json_tricks import loads
json_str = loads(json_str)
这确实假设module_name.test_class。MyTestCls可以导入,并且没有以不兼容的方式进行更改。您将返回一个实例,而不是某个字典或其他东西,它应该是您转储的实例的相同副本。
如果你想自定义一些东西是如何(反)序列化的,你可以添加特殊的方法到你的类,像这样:
class CustomEncodeCls:
def __init__(self):
self.relevant = 42
self.irrelevant = 37
def __json_encode__(self):
# should return primitive, serializable types like dict, list, int, string, float...
return {'relevant': self.relevant}
def __json_decode__(self, **attrs):
# should initialize all properties; note that __init__ is not called implicitly
self.relevant = attrs['relevant']
self.irrelevant = 12
其中仅序列化部分属性参数,作为示例。
作为免费的奖励,你可以获得numpy数组、日期和时间、有序地图的(反)序列化,以及在json中包含注释的能力。
免责声明:我创建了json_tricks,因为我遇到了与您相同的问题。
你们为什么要把事情搞得这么复杂?这里有一个简单的例子:
#!/usr/bin/env python3
import json
from dataclasses import dataclass
@dataclass
class Person:
first: str
last: str
age: int
@property
def __json__(self):
return {
"name": f"{self.first} {self.last}",
"age": self.age
}
john = Person("John", "Doe", 42)
print(json.dumps(john, indent=4, default=lambda x: x.__json__))
这样你也可以序列化嵌套类,因为__json__返回一个python对象而不是字符串。不需要使用JSONEncoder,因为使用简单lambda的默认参数也可以很好地工作。
我使用@property代替了一个简单的函数,因为这样感觉更自然和现代。@dataclass也只是一个例子,它也适用于“普通”类。