如何使 Python 中的 json.dumps 忽略不可序列化的字段
How to make json.dumps in Python ignore a non-serializable field
我正在尝试使用 Construct2.9 库序列化解析某些二进制数据的输出。我想将结果序列化为 JSON.
packet
是 Construct class Container
.
的实例
显然它包含一个 BytesIO
类型的隐藏 _io
- 请参阅下面 dict(packet)
的输出:
{
'packet_length': 76, 'uart_sent_time': 1, 'frame_number': 42958,
'subframe_number': 0, 'checksum': 33157, '_io': <_io.BytesIO object at 0x7f81c3153728>,
'platform':661058, 'sync': 506660481457717506, 'frame_margin': 20642,
'num_tlvs': 1, 'track_process_time': 593, 'chirp_margin': 78,
'timestamp': 2586231182, 'version': 16908293
}
现在,调用 json.dumps(packet)
显然会导致 TypeError:
...
File "/usr/lib/python3.5/json/__init__.py", line 237, in dumps
**kw).encode(obj)
File "/usr/lib/python3.5/json/encoder.py", line 198, in encode
chunks = self.iterencode(o, _one_shot=True)
File "/usr/lib/python3.5/json/encoder.py", line 256, in iterencode
return _iterencode(o, 0)
File "/usr/lib/python3.5/json/encoder.py", line 179, in default
raise TypeError(repr(o) + " is not JSON serializable")
TypeError: <_io.BytesIO object at 0x7f81c3153728> is not JSON serializable
然而让我感到困惑的是 运行 json.dumps(packet, skipkeys=True)
导致完全相同的错误,而我希望它跳过 _io
字段。这里有什么问题?为什么 skipkeys
不允许我跳过 _io
字段?
我通过覆盖 JSONEncoder
并为 BytesIO
类型的字段返回 None
让代码工作,但这意味着我的序列化字符串包含大量 "_io": null
元素,我根本不想拥有...
带有前导 _
下划线的键并不是真正的 'hidden',它们只是 JSON 的更多字符串。 Construct Container
class 只是一个带顺序的字典,_io
键对于 class.
没有什么特别之处
您有两个选择:
- 实现一个
default
挂钩,它只是 returns 一个替换值。
- 在序列化之前过滤掉您知道不能工作的键值对。
也许还有三分之一,但是随便浏览一下 Construct 项目页面并没有告诉我它是否可用:有 Construct 输出 JSON 或至少一个 JSON 兼容的字典,也许通过使用适配器。
默认挂钩无法阻止将 _io
键添加到输出中,但至少可以让您避免错误:
json.dumps(packet, default=lambda o: '<not serializable>')
过滤可以递归完成; @functools.singledispatch()
decorator 可以帮助保持这样的代码干净:
from functools import singledispatch
_cant_serialize = object()
@singledispatch
def json_serializable(object, skip_underscore=False):
"""Filter a Python object to only include serializable object types
In dictionaries, keys are converted to strings; if skip_underscore is true
then keys starting with an underscore ("_") are skipped.
"""
# default handler, called for anything without a specific
# type registration.
return _cant_serialize
@json_serializable.register(dict)
def _handle_dict(d, skip_underscore=False):
converted = ((str(k), json_serializable(v, skip_underscore))
for k, v in d.items())
if skip_underscore:
converted = ((k, v) for k, v in converted if k[:1] != '_')
return {k: v for k, v in converted if v is not _cant_serialize}
@json_serializable.register(list)
@json_serializable.register(tuple)
def _handle_sequence(seq, skip_underscore=False):
converted = (json_serializable(v, skip_underscore) for v in seq)
return [v for v in converted if v is not _cant_serialize]
@json_serializable.register(int)
@json_serializable.register(float)
@json_serializable.register(str)
@json_serializable.register(bool) # redudant, supported as int subclass
@json_serializable.register(type(None))
def _handle_default_scalar_types(value, skip_underscore=False):
return value
我在上面的实现中还有一个额外的 skip_underscore
参数,以明确跳过开头有 _
字符的键。这将有助于跳过 Construct 库正在使用的所有其他 'hidden' 属性。
因为Container
是一个dict
子class,上面的代码会自动处理packet
.
这样的实例
skipkeys
doesn't do what you might think it does - it instructs the json.JSONEncoder
跳过不属于 basic 类型的键,而不是键的值 - 即如果你有一个 dict
{object(): "foobar"}
它将跳过 object()
键,而没有 skipkeys
设置为 True
它将引发 TypeError
.
您可以重载 JSONEncoder.iterencode()
(及其弱点)并在那里执行前瞻性过滤,但您最终会重写 json
模块,在这个过程中减慢它的速度您将无法从编译的部分中受益。我建议您通过迭代过滤预处理您的数据,并在最终 JSON 中跳过您不想要的 keys/types。然后 json
模块应该能够在没有任何额外指令的情况下处理它。类似于:
import collections
class SkipFilter(object):
def __init__(self, types=None, keys=None, allow_empty=False):
self.types = tuple(types or [])
self.keys = set(keys or [])
self.allow_empty = allow_empty # if True include empty filtered structures
def filter(self, data):
if isinstance(data, collections.Mapping):
result = {} # dict-like, use dict as a base
for k, v in data.items():
if k in self.keys or isinstance(v, self.types): # skip key/type
continue
try:
result[k] = self.filter(v)
except ValueError:
pass
if result or self.allow_empty:
return result
elif isinstance(data, collections.Sequence):
result = [] # a sequence, use list as a base
for v in data:
if isinstance(v, self.types): # skip type
continue
try:
result.append(self.filter(v))
except ValueError:
pass
if result or self.allow_empty:
return result
else: # we don't know how to traverse this structure...
return data # return it as-is, hope for the best...
raise ValueError
然后创建您的过滤器:
import io
preprocessor = SkipFilter([io.BytesIO], ["_io"]) # double-whammy skip of io.BytesIO
在这种情况下,仅按类型跳过就足够了,但如果 _io
键包含一些其他不需要的数据,这保证它不会出现在最终结果中。无论如何,您可以在将数据传递给 JSONEncoder
:
之前过滤数据
import json
json_data = json.dumps(preprocessor.filter(packet)) # no _io keys or io.BytesIO data...
当然,如果您的结构包含一些其他奇异数据或在 JSON 中表示的数据根据其类型不同,这种方法可能会搞砸它,因为它将所有映射变成 dict
和所有序列到 list
。但是,对于一般用途,这应该绰绰有余。
忽略 non-serializable 字段需要繁重的额外逻辑,正如之前所有答案中正确指出的那样。
如果您真的不需要排除该字段,那么您可以生成一个默认值:
def safe_serialize(obj):
default = lambda o: f"<<non-serializable: {type(o).__qualname__}>>"
return json.dumps(obj, default=default)
obj = {"a": 1, "b": bytes()} # bytes is non-serializable by default
print(safe_serialize(obj))
这将产生这个结果:
{"a": 1, "b": "<<non-serializable: bytes>>"}
此代码将打印类型名称,如果您想稍后实现自定义序列化程序,这可能会有用。
我正在尝试使用 Construct2.9 库序列化解析某些二进制数据的输出。我想将结果序列化为 JSON.
packet
是 Construct class Container
.
显然它包含一个 BytesIO
类型的隐藏 _io
- 请参阅下面 dict(packet)
的输出:
{
'packet_length': 76, 'uart_sent_time': 1, 'frame_number': 42958,
'subframe_number': 0, 'checksum': 33157, '_io': <_io.BytesIO object at 0x7f81c3153728>,
'platform':661058, 'sync': 506660481457717506, 'frame_margin': 20642,
'num_tlvs': 1, 'track_process_time': 593, 'chirp_margin': 78,
'timestamp': 2586231182, 'version': 16908293
}
现在,调用 json.dumps(packet)
显然会导致 TypeError:
...
File "/usr/lib/python3.5/json/__init__.py", line 237, in dumps
**kw).encode(obj)
File "/usr/lib/python3.5/json/encoder.py", line 198, in encode
chunks = self.iterencode(o, _one_shot=True)
File "/usr/lib/python3.5/json/encoder.py", line 256, in iterencode
return _iterencode(o, 0)
File "/usr/lib/python3.5/json/encoder.py", line 179, in default
raise TypeError(repr(o) + " is not JSON serializable")
TypeError: <_io.BytesIO object at 0x7f81c3153728> is not JSON serializable
然而让我感到困惑的是 运行 json.dumps(packet, skipkeys=True)
导致完全相同的错误,而我希望它跳过 _io
字段。这里有什么问题?为什么 skipkeys
不允许我跳过 _io
字段?
我通过覆盖 JSONEncoder
并为 BytesIO
类型的字段返回 None
让代码工作,但这意味着我的序列化字符串包含大量 "_io": null
元素,我根本不想拥有...
带有前导 _
下划线的键并不是真正的 'hidden',它们只是 JSON 的更多字符串。 Construct Container
class 只是一个带顺序的字典,_io
键对于 class.
您有两个选择:
- 实现一个
default
挂钩,它只是 returns 一个替换值。 - 在序列化之前过滤掉您知道不能工作的键值对。
也许还有三分之一,但是随便浏览一下 Construct 项目页面并没有告诉我它是否可用:有 Construct 输出 JSON 或至少一个 JSON 兼容的字典,也许通过使用适配器。
默认挂钩无法阻止将 _io
键添加到输出中,但至少可以让您避免错误:
json.dumps(packet, default=lambda o: '<not serializable>')
过滤可以递归完成; @functools.singledispatch()
decorator 可以帮助保持这样的代码干净:
from functools import singledispatch
_cant_serialize = object()
@singledispatch
def json_serializable(object, skip_underscore=False):
"""Filter a Python object to only include serializable object types
In dictionaries, keys are converted to strings; if skip_underscore is true
then keys starting with an underscore ("_") are skipped.
"""
# default handler, called for anything without a specific
# type registration.
return _cant_serialize
@json_serializable.register(dict)
def _handle_dict(d, skip_underscore=False):
converted = ((str(k), json_serializable(v, skip_underscore))
for k, v in d.items())
if skip_underscore:
converted = ((k, v) for k, v in converted if k[:1] != '_')
return {k: v for k, v in converted if v is not _cant_serialize}
@json_serializable.register(list)
@json_serializable.register(tuple)
def _handle_sequence(seq, skip_underscore=False):
converted = (json_serializable(v, skip_underscore) for v in seq)
return [v for v in converted if v is not _cant_serialize]
@json_serializable.register(int)
@json_serializable.register(float)
@json_serializable.register(str)
@json_serializable.register(bool) # redudant, supported as int subclass
@json_serializable.register(type(None))
def _handle_default_scalar_types(value, skip_underscore=False):
return value
我在上面的实现中还有一个额外的 skip_underscore
参数,以明确跳过开头有 _
字符的键。这将有助于跳过 Construct 库正在使用的所有其他 'hidden' 属性。
因为Container
是一个dict
子class,上面的代码会自动处理packet
.
skipkeys
doesn't do what you might think it does - it instructs the json.JSONEncoder
跳过不属于 basic 类型的键,而不是键的值 - 即如果你有一个 dict
{object(): "foobar"}
它将跳过 object()
键,而没有 skipkeys
设置为 True
它将引发 TypeError
.
您可以重载 JSONEncoder.iterencode()
(及其弱点)并在那里执行前瞻性过滤,但您最终会重写 json
模块,在这个过程中减慢它的速度您将无法从编译的部分中受益。我建议您通过迭代过滤预处理您的数据,并在最终 JSON 中跳过您不想要的 keys/types。然后 json
模块应该能够在没有任何额外指令的情况下处理它。类似于:
import collections
class SkipFilter(object):
def __init__(self, types=None, keys=None, allow_empty=False):
self.types = tuple(types or [])
self.keys = set(keys or [])
self.allow_empty = allow_empty # if True include empty filtered structures
def filter(self, data):
if isinstance(data, collections.Mapping):
result = {} # dict-like, use dict as a base
for k, v in data.items():
if k in self.keys or isinstance(v, self.types): # skip key/type
continue
try:
result[k] = self.filter(v)
except ValueError:
pass
if result or self.allow_empty:
return result
elif isinstance(data, collections.Sequence):
result = [] # a sequence, use list as a base
for v in data:
if isinstance(v, self.types): # skip type
continue
try:
result.append(self.filter(v))
except ValueError:
pass
if result or self.allow_empty:
return result
else: # we don't know how to traverse this structure...
return data # return it as-is, hope for the best...
raise ValueError
然后创建您的过滤器:
import io
preprocessor = SkipFilter([io.BytesIO], ["_io"]) # double-whammy skip of io.BytesIO
在这种情况下,仅按类型跳过就足够了,但如果 _io
键包含一些其他不需要的数据,这保证它不会出现在最终结果中。无论如何,您可以在将数据传递给 JSONEncoder
:
import json
json_data = json.dumps(preprocessor.filter(packet)) # no _io keys or io.BytesIO data...
当然,如果您的结构包含一些其他奇异数据或在 JSON 中表示的数据根据其类型不同,这种方法可能会搞砸它,因为它将所有映射变成 dict
和所有序列到 list
。但是,对于一般用途,这应该绰绰有余。
忽略 non-serializable 字段需要繁重的额外逻辑,正如之前所有答案中正确指出的那样。
如果您真的不需要排除该字段,那么您可以生成一个默认值:
def safe_serialize(obj):
default = lambda o: f"<<non-serializable: {type(o).__qualname__}>>"
return json.dumps(obj, default=default)
obj = {"a": 1, "b": bytes()} # bytes is non-serializable by default
print(safe_serialize(obj))
这将产生这个结果:
{"a": 1, "b": "<<non-serializable: bytes>>"}
此代码将打印类型名称,如果您想稍后实现自定义序列化程序,这可能会有用。