如何使 Python 中的 json.dumps 忽略不可序列化的字段

Posted

技术标签:

【中文标题】如何使 Python 中的 json.dumps 忽略不可序列化的字段【英文标题】:How to make json.dumps in Python ignore a non-serializable field 【发布时间】:2019-01-11 10:35:40 【问题描述】:

我正在尝试使用 Construct2.9 库对解析一些二进制数据的输出进行序列化。我想将结果序列化为 JSON。

packet 是 Construct 类 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 元素,我根本不想拥有这些元素...

【问题讨论】:

skipkeys 只忽略非原始键,而不是值 @EdwardMinnix 啊,我知道我遗漏了一些东西……有没有办法完全跳过某个字段的编码? @mz8i 你知道为什么 Construct2.9 的变化会产生_io吗?多年来我一直在使用 Construct 并且现在很好地转换为 json,直到 2.9 更新。 【参考方案1】:

带有前导_ 下划线的键并不是真正的“隐藏”,它们只是 JSON 的更多字符串。 Construct Container 类只是一个带排序的字典,_io 键对该类没有什么特别的。

你有两个选择:

实现一个 default 钩子,它只返回一个替换值。 在序列化之前过滤掉你知道不能工作的键值对。

也许还有第三个,但对 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 库正在使用的所有其他“隐藏”属性。

由于Containerdict的子类,上面的代码会自动处理packet等实例。

【讨论】:

@MartijnPieters 这似乎不适用于嵌套对象 - 是否有一种漂亮的通用方法可以实现这一点并让它适用于任何对象?例如我有一个 Author 对象,它有一个 Publications 属性,它是一个发布对象数组...有人吗? @MarkZhukovsky:这确实支持嵌套对象,因为每个注册的函数都会递归。所以处理列表中的字典是因为处理列表的函数在列表的每个元素上调用json_serializable()。您只需为其他类型注册附加功能,例如您的作者类型。确保为该类型的每个属性调用json_serializable()。问题中的对象是dict 的子类,这就是为什么我们不必为它注册任何特殊的东西,但显然你的Author 类型不是。 @MartijnPieters 感谢您的快速回复。我确实可以通过添加作者和出版物来让它工作,而且我对嵌套评论很不满意。我所做的只是复制 handle_dict 的函数并将 d.items() 更改为 d.__dict_.items() 并且它起作用了。有没有一种方法可以通用地处理任何类类型来执行此操作,因此不需要手动添加特定类?我来自 diff 语言,所以感谢您的耐心等待。 @MarkZhukovsky:您可以从基类派生您的类(甚至不需要任何方法或属性),然后注册它。我会使用return json_serializable(vars(instance)) 作为注册的函数体。【参考方案2】:

skipkeys 并没有像你想象的那样做 - 它指示 json.JSONEncoder 跳过不是 basic 类型的键,而不是键的值 - 即如果你有一个dict object(): "foobar" 它会跳过object() 键,而没有skipkeys 设置为True 它会引发TypeError

您可以重载JSONEncoder.iterencode()(及其弱点)并在那里执行前瞻过滤,但您最终会重写json 模块,因为您无法在此过程中减慢它的速度从编译的部分中受益。我建议您通过迭代过滤预处理数据并跳过最终 JSON 中不需要的键/类型。然后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。但是,对于一般用途,这应该绰绰有余了。

【讨论】:

【参考方案3】:

忽略不可序列化的字段需要大量的额外逻辑,正如所有先前答案中正确指出的那样。

如果你真的不需要排除该字段,那么你可以生成一个默认值来代替:

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>>"

此代码将打印类型名称,如果您想稍后实现自定义序列化程序,这可能会很有用。

【讨论】:

以上是关于如何使 Python 中的 json.dumps 忽略不可序列化的字段的主要内容,如果未能解决你的问题,请参考以下文章

python中的json.dump()和json.dumps()有啥区别?

python中json模块dumps和loads的区分

[转]python json.dumps 中的ensure_ascii 参数引起的中文编码

python json.dumps 中的ensure_ascii 参数引起的中文编码问题

python json.dumps 中的ensure_ascii 参数引起的中文编码问题

python中的dumps和loads区别