【问题标题】:Serializing a Python namedtuple to json将 Python namedtuple 序列化为 json
【发布时间】:2011-08-19 21:27:26
【问题描述】:

namedtuple 序列化为 json 并保留字段名称的推荐方法是什么?

namedtuple 序列化为 json 只会导致值被序列化,而字段名称在翻译中会丢失。我希望在 json 化时也保留这些字段,因此执行以下操作:

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

上面的内容按照我的预期序列化为 json,并且在我使用的其他地方(属性访问等)表现为 namedtuple,除了在迭代它时出现非元组类似的结果(这对我的用例来说很好)。

在保留字段名的情况下转换为json的“正确方法”是什么?

【问题讨论】:

标签: python json namedtuple


【解决方案1】:

如果您要序列化的只是一个namedtuple,则使用其_asdict() 方法将有效(Python >= 2.7)

>>> from collections import namedtuple
>>> import json
>>> FB = namedtuple("FB", ("foo", "bar"))
>>> fb = FB(123, 456)
>>> json.dumps(fb._asdict())
'{"foo": 123, "bar": 456}'

【讨论】:

  • 在 Windows 上以 Python 2.7 (x64) 运行该代码时出现 AttributeError: 'FB' object has no attribute 'dict'。但是 fb._asdict() 工作正常。
  • fb._asdict()vars(fb) 会更好。
  • @jpmc26:您不能在没有__dict__ 的对象上使用vars
  • @Rufflewind 你也不能在上面使用__dict__。 =)
  • 在 python 3 中 __dict__ 已被删除。 _asdict 似乎对两者都有效。
【解决方案2】:

这很棘手,因为namedtuple() 是一个工厂,它返回从tuple 派生的新类型。一种方法是让您的类也继承自 UserDict.DictMixin,但 tuple.__getitem__ 已经定义,并且需要一个整数来表示元素的位置,而不是其属性的名称:

>>> f = foobar('a', 1)
>>> f[0]
'a'

从本质上讲,namedtuple 非常适合 JSON,因为它实际上是一个自定义构建的类型,其键名作为类型定义的一部分是固定的,不像字典中键名是存储在实例中。这可以防止您“往返”命名元组,例如如果没有其他一些信息,您无法将字典解码回命名元组,例如 dict {'a': 1, '#_type': 'foobar'} 中的特定于应用程序的类型标记,这有点 hacky。

这并不理想,但是如果您只需要将命名元组编码 到字典中,另一种方法是扩展或修改您的 JSON 编码器以对这些类型进行特殊处理。这是一个子类化 Python json.JSONEncoder 的示例。这解决了确保嵌套命名元组正确转换为字典的问题:

from collections import namedtuple
from json import JSONEncoder

class MyEncoder(JSONEncoder):

    def _iterencode(self, obj, markers=None):
        if isinstance(obj, tuple) and hasattr(obj, '_asdict'):
            gen = self._iterencode_dict(obj._asdict(), markers)
        else:
            gen = JSONEncoder._iterencode(self, obj, markers)
        for chunk in gen:
            yield chunk

class foobar(namedtuple('f', 'foo, bar')):
    pass

enc = MyEncoder()
for obj in (foobar('a', 1), ('a', 1), {'outer': foobar('x', 'y')}):
    print enc.encode(obj)

{"foo": "a", "bar": 1}
["a", 1]
{"outer": {"foo": "x", "bar": "y"}}

【讨论】:

  • 从本质上讲,namedtuple 非常适合 JSON,因为它实际上是一种自定义类型,其键名作为类型定义的一部分是固定的,不像字典中的键名存储在实例中。 非常有见地的评论。我没有想到这一点。谢谢。我喜欢 namedtuples,因为它们提供了一个很好的不可变结构 with 属性命名方便。我会接受你的回答。话虽如此,Java 的序列化机制提供了对如何对象序列化的更多控制,我很想知道为什么 Python 中似乎不存在这样的钩子。
  • 这是我的第一种方法,但它实际上不起作用(无论如何对我来说)。
  • >>> json.dumps(foobar('x', 'y'), cls=MyEncoder) <<< '["x", "y"]'
  • 啊,在python 2.7+中,_iterencode不再是JSONEncoder的方法了。
  • @calvin 谢谢,我发现 namedtuple 也很有用,希望有更好的解决方案将其递归编码为 JSON。 @zeekay 是的,似乎在 2.7+ 中他们隐藏了它,所以它不能再被覆盖。这令人失望。
【解决方案3】:

看起来您以前可以继承 simplejson.JSONEncoder 来完成这项工作,但使用最新的 simplejson 代码,情况已不再如此:您必须实际修改项目代码。我看不出为什么 simplejson 不应该支持 namedtuples,所以我分叉了这个项目,添加了 namedtuple 支持,我是currently waiting for my branch to be pulled back into the main project。如果您现在需要修复,只需从我的 fork 中提取即可。

编辑:看起来simplejson 的最新版本现在通过namedtuple_as_object 选项原生支持此功能,默认为True

【讨论】:

  • 您的编辑是正确的答案。 simplejson 序列化命名元组的方式与 json 不同(我认为:更好)。这确实使模式:“try: import simplejson as json except: import json”,有风险,因为根据是否安装了 simplejson,您可能会在某些机器上获得不同的行为。出于这个原因,我现在在我的很多设置文件中都需要 simplejson 并放弃该模式。
  • @marr75 - ujson 同上,在这种边缘情况下更加奇怪和不可预测......
  • 我能够使用:simplejson.dumps(my_tuple, indent=4) 将递归命名元组序列化为(漂亮打印的)json
【解决方案4】:

我为此编写了一个库:https://github.com/ltworf/typedload

它可以往返于命名元组。

它支持相当复杂的嵌套结构,包括列表、集合、枚举、联合、默认值。它应该涵盖最常见的情况。

edit:该库还支持 dataclass 和 attr 类。

【讨论】:

    【解决方案5】:

    它递归地将 namedTuple 数据转换为 json。

    print(m1)
    ## Message(id=2, agent=Agent(id=1, first_name='asd', last_name='asd', mail='2@mai.com'), customer=Customer(id=1, first_name='asd', last_name='asd', mail='2@mai.com', phone_number=123123), type='image', content='text', media_url='h.com', la=123123, ls=4512313)
    
    def reqursive_to_json(obj):
        _json = {}
    
        if isinstance(obj, tuple):
            datas = obj._asdict()
            for data in datas:
                if isinstance(datas[data], tuple):
                    _json[data] = (reqursive_to_json(datas[data]))
                else:
                     print(datas[data])
                    _json[data] = (datas[data])
        return _json
    
    data = reqursive_to_json(m1)
    print(data)
    {'agent': {'first_name': 'asd',
    'last_name': 'asd',
    'mail': '2@mai.com',
    'id': 1},
    'content': 'text',
    'customer': {'first_name': 'asd',
    'last_name': 'asd',
    'mail': '2@mai.com',
    'phone_number': 123123,
    'id': 1},
    'id': 2,
    'la': 123123,
    'ls': 4512313,
    'media_url': 'h.com',
    'type': 'image'}
    

    【讨论】:

    • +1 我做的几乎一样。但是你的回报是一个字典而不是 json。你必须有“not”,如果你的对象中的值是布尔值,它不会被转换为true。我认为转换为dict更安全,然后使用json.dumps转换为json。
    【解决方案6】:

    还有一个更方便的解决方案是使用装饰器(它使用受保护的字段_fields)。

    Python 2.7+:

    import json
    from collections import namedtuple, OrderedDict
    
    def json_serializable(cls):
        def as_dict(self):
            yield OrderedDict(
                (name, value) for name, value in zip(
                    self._fields,
                    iter(super(cls, self).__iter__())))
        cls.__iter__ = as_dict
        return cls
    
    #Usage:
    
    C = json_serializable(namedtuple('C', 'a b c'))
    print json.dumps(C('abc', True, 3.14))
    
    # or
    
    @json_serializable
    class D(namedtuple('D', 'a b c')):
        pass
    
    print json.dumps(D('abc', True, 3.14))
    

    Python 3.6.6+:

    import json
    from typing import TupleName
    
    def json_serializable(cls):
        def as_dict(self):
            yield {name: value for name, value in zip(
                self._fields,
                iter(super(cls, self).__iter__()))}
        cls.__iter__ = as_dict
        return cls
    
    # Usage:
    
    @json_serializable
    class C(NamedTuple):
        a: str
        b: bool
        c: float
    
    print(json.dumps(C('abc', True, 3.14))
    

    【讨论】:

    • 不要那样做,他们一直在改变内部 API。我的 typedload 库有几个不同 py 版本的案例。
    • 是的,很清楚。但是,没有人应该在未经测试的情况下迁移到更新的 Python 版本。而且,其他解决方案使用_asdict,它也是“受保护”类成员。
    • LtWorf,您的库是 GPL 并且不适用于frozensets
    • @LtWorf 你的库也使用_fields ;-) github.com/ltworf/typedload/blob/master/typedload/datadumper.py 它是namedtuple 的公共API 的一部分,实际上:docs.python.org/3.7/library/… 人们对下划线感到困惑(难怪!)。这是糟糕的设计,但我不知道他们还有什么其他选择。
    • 什么东西?什么时候?你能引用发行说明吗?
    【解决方案7】:

    jsonplus 库为 NamedTuple 实例提供了一个序列化程序。如果需要,使用它的兼容模式输出简单的对象,但更喜欢默认,因为它有助于解码。

    【讨论】:

    • 我查看了此处的其他解决方案,发现只需添加此依赖项即可节省大量时间。特别是因为我有一个 NamedTuples 列表,我需要在会话中将其作为 json 传递。 jsonplus 让您基本上可以使用 .dumps().loads() 将命名元组列表导入和导出 json,无需配置它就可以工作。
    【解决方案8】:

    这是一个老问题。然而:

    对所有有相同问题的人的建议,请仔细考虑使用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())
    

    我尝试使用 default 可调用 kwarg 到 dumps 来进行 to_dict() 调用(如果可用),但由于 NamedTuple 可转换为列表,因此没有调用。

    【讨论】:

    • _asdict 是 namedtuple 公共 API 的一部分。他们解释了下划线docs.python.org/3.7/library/…的原因“命名元组除了继承自元组的方法外,还支持三个附加方法和两个属性。为防止与字段名冲突,方法名和属性名以下划线开头。”跨度>
    • @quant_dev 谢谢,我没有看到那个解释。这不是 api 稳定性的保证,但它有助于使这些方法更值得信赖。我确实喜欢明确的 to_dict 可读性,但我可以看到它似乎重新实现了 _as_dict
    【解决方案9】:

    使用原生 python json 库无法正确序列化 namedtuples。它将始终将元组视为列表,并且不可能覆盖默认序列化程序来更改此行为。如果对象是嵌套的,那就更糟了。

    最好使用更健壮的库,例如 orjson

    import orjson
    from typing import NamedTuple
    
    class Rectangle(NamedTuple):
        width: int
        height: int
    
    def default(obj):
        if hasattr(obj, '_asdict'):
            return obj._asdict()
    
    rectangle = Rectangle(width=10, height=20)
    print(orjson.dumps(rectangle, default=default))
    

    =>

    {
        "width":10,
        "height":20
    }
    

    【讨论】:

    • 我也是orjson的粉丝。
    【解决方案10】:

    这是我对这个问题的看法。它序列化 NamedTuple,处理折叠的 NamedTuple 和其中的 Lists

    def recursive_to_dict(obj: Any) -> dict:
    _dict = {}
    
    if isinstance(obj, tuple):
        node = obj._asdict()
        for item in node:
            if isinstance(node[item], list): # Process as a list
                _dict[item] = [recursive_to_dict(x) for x in (node[item])]
            elif getattr(node[item], "_asdict", False): # Process as a NamedTuple
                _dict[item] = recursive_to_dict(node[item])
            else: # Process as a regular element
                _dict[item] = (node[item])
    return _dict
    

    【讨论】:

      【解决方案11】:

      simplejson.dump() 代替 json.dump 完成这项工作。不过可能会慢一些。

      【讨论】:

        猜你喜欢
        • 2017-09-03
        • 2020-03-15
        • 1970-01-01
        • 2017-01-20
        • 2015-07-27
        • 2016-12-08
        • 2016-08-21
        • 2020-10-06
        • 1970-01-01
        相关资源
        最近更新 更多