将Python的namedtuple序列化为json

108

如何将 namedtuple 序列化为JSON并保留字段名?

namedtuple 序列化为 JSON 时,只有值被序列化,字段名称会丢失。我希望在进行 JSON 序列化时也能够保留字段名,因此我执行了以下操作:

class foobar(namedtuple('f', 'foo, bar')):
    __slots__ = ()
    def __iter__(self):
        yield self._asdict()

上述内容在序列化成JSON后符合我的预期,并在我使用时(如属性访问等)与namedtuple一样工作,除了在迭代时出现非元组结果的情况(对于我的用例来说没问题)。

有没有将字段名保留转换为JSON的“正确方法”?


针对Python 2.7版本:https://dev59.com/OmQn5IYBdhLWcg3wTloS - lowtech
12个回答

1
这是一个老问题。然而:对于所有有同样问题的人,建议仔细考虑使用NamedTuple的任何私有或内部特性,因为它们以前已经发生过变化,并且将来还会再次变化。例如,如果你的NamedTuple是一个平面值对象,你只关心将其序列化而不关心它嵌套到另一个对象中的情况,那么你可以避免使用__dict__被删除或_as_dict()改变所带来的麻烦,只需做类似以下的事情(是Python 3,因为这个答案是针对现在的):
from typing import NamedTuple

class ApiListRequest(NamedTuple):
  group: str="default"
  filter: str="*"

  def to_dict(self):
    return {
      'group': self.group,
      'filter': self.filter,
    }

  def to_json(self):
    return json.dumps(self.to_dict())

我尝试使用dumpsdefault可调用kwarg,以便在可能的情况下执行to_dict()调用,但由于NamedTuple可以转换为列表,因此未被调用。

4
_asdict是namedtuple公共API的一部分。它们解释了下划线的原因。除了继承自元组的方法外,命名元组还支持三个额外的方法和两个属性。为了避免与字段名称发生冲突,方法和属性名称以下划线开头。 - quant_dev
@quant_dev 谢谢,我没看到那个解释。这并不是 API 稳定性的保证,但它有助于使这些方法更加可靠。我确实喜欢明确的 to_dict 可读性,但我可以看出似乎是重新实现了 _as_dict。 - dlamblin

0

我知道这是一个非常旧的线程,但对于这个问题,我想到的一个解决办法是使用类似的自定义方法来修补或覆盖函数json.encoder._make_iterencode,并在其中扩展以单独处理命名元组。我不确定这是否是一个好的做法,或者是否有一种更安全的标准方法来进行修补:

def _make_iterencode(markers, _default, _encoder, _indent, _floatstr,
        _key_separator, _item_separator, _sort_keys, _skipkeys, _one_shot,
        ValueError=ValueError,
        dict=dict,
        float=float,
        id=id,
        int=int,
        isinstance=isinstance,
        list=list,
        str=str,
        tuple=tuple,
        _intstr=int.__repr__,
    ):

    if _indent is not None and not isinstance(_indent, str):
        _indent = ' ' * _indent

    def _iterencode_list(lst, _current_indent_level):
        if not lst:
            yield '[]'
            return
        if markers is not None:
            markerid = id(lst)
            if markerid in markers:
                raise ValueError("Circular reference detected")
            markers[markerid] = lst
        buf = '['
        if _indent is not None:
            _current_indent_level += 1
            newline_indent = '\n' + _indent * _current_indent_level
            separator = _item_separator + newline_indent
            buf += newline_indent
        else:
            newline_indent = None
            separator = _item_separator
        first = True
        for value in lst:
            if first:
                first = False
            else:
                buf = separator
            if isinstance(value, str):
                yield buf + _encoder(value)
            elif value is None:
                yield buf + 'null'
            elif value is True:
                yield buf + 'true'
            elif value is False:
                yield buf + 'false'
            elif isinstance(value, int):
                # Subclasses of int/float may override __repr__, but we still
                # want to encode them as integers/floats in JSON. One example
                # within the standard library is IntEnum.
                yield buf + _intstr(value)
            elif isinstance(value, float):
                # see comment above for int
                yield buf + _floatstr(value)
            else:
                yield buf

                # EDIT
                ##################
                if isinstance(value, tuple) and hasattr(value, '_asdict'):
                    value = value._asdict()
                    chunks = _iterencode_dict(value, _current_indent_level)
                ##################

                elif isinstance(value, (list, tuple)):
                    chunks = _iterencode_list(value, _current_indent_level)
                elif isinstance(value, dict):
                    chunks = _iterencode_dict(value, _current_indent_level)
                else:
                    chunks = _iterencode(value, _current_indent_level)
                yield from chunks
        if newline_indent is not None:
            _current_indent_level -= 1
            yield '\n' + _indent * _current_indent_level
        yield ']'
        if markers is not None:
            del markers[markerid]

    def _iterencode_dict(dct, _current_indent_level):
        if not dct:
            yield '{}'
            return
        if markers is not None:
            markerid = id(dct)
            if markerid in markers:
                raise ValueError("Circular reference detected")
            markers[markerid] = dct
        yield '{'
        if _indent is not None:
            _current_indent_level += 1
            newline_indent = '\n' + _indent * _current_indent_level
            item_separator = _item_separator + newline_indent
            yield newline_indent
        else:
            newline_indent = None
            item_separator = _item_separator
        first = True
        if _sort_keys:
            items = sorted(dct.items())
        else:
            items = dct.items()
        for key, value in items:
            if isinstance(key, str):
                pass
            # JavaScript is weakly typed for these, so it makes sense to
            # also allow them.  Many encoders seem to do something like this.
            elif isinstance(key, float):
                # see comment for int/float in _make_iterencode
                key = _floatstr(key)
            elif key is True:
                key = 'true'
            elif key is False:
                key = 'false'
            elif key is None:
                key = 'null'
            elif isinstance(key, int):
                # see comment for int/float in _make_iterencode
                key = _intstr(key)
            elif _skipkeys:
                continue
            else:
                raise TypeError(f'keys must be str, int, float, bool or None, '
                                f'not {key.__class__.__name__}')
            if first:
                first = False
            else:
                yield item_separator
            yield _encoder(key)
            yield _key_separator
            if isinstance(value, str):
                yield _encoder(value)
            elif value is None:
                yield 'null'
            elif value is True:
                yield 'true'
            elif value is False:
                yield 'false'
            elif isinstance(value, int):
                # see comment for int/float in _make_iterencode
                yield _intstr(value)
            elif isinstance(value, float):
                # see comment for int/float in _make_iterencode
                yield _floatstr(value)
            else:

                # EDIT
                ###############
                if isinstance(value, tuple) and hasattr(value, '_asdict'):
                    value = value._asdict()
                    chunks = _iterencode_dict(value, _current_indent_level)
                ###############

                elif isinstance(value, (list, tuple)):
                    chunks = _iterencode_list(value, _current_indent_level)
                elif isinstance(value, dict):
                    chunks = _iterencode_dict(value, _current_indent_level)
                else:
                    chunks = _iterencode(value, _current_indent_level)
                yield from chunks
        if newline_indent is not None:
            _current_indent_level -= 1
            yield '\n' + _indent * _current_indent_level
        yield '}'
        if markers is not None:
            del markers[markerid]

    def _iterencode(o, _current_indent_level):
        if isinstance(o, str):
            yield _encoder(o)
        elif o is None:
            yield 'null'
        elif o is True:
            yield 'true'
        elif o is False:
            yield 'false'
        elif isinstance(o, int):
            # see comment for int/float in _make_iterencode
            yield _intstr(o)
        elif isinstance(o, float):
            # see comment for int/float in _make_iterencode
            yield _floatstr(o)

        # EDIT
        ##################
        elif isinstance(o, tuple) and hasattr(o, '_asdict'):
            o = o._asdict()
            yield from _iterencode_dict(o, _current_indent_level)
        ##################

        elif isinstance(o, (list, tuple)):
            yield from _iterencode_list(o, _current_indent_level)
        elif isinstance(o, dict):
            yield from _iterencode_dict(o, _current_indent_level)
        else:
            if markers is not None:
                markerid = id(o)
                if markerid in markers:
                    raise ValueError("Circular reference detected")
                markers[markerid] = o
            o = _default(o)
            yield from _iterencode(o, _current_indent_level)
            if markers is not None:
                del markers[markerid]
    return _iterencode

# alters the json lib
json.encoder._make_iterencode = _make_iterencode


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接