将Python namedtuple序列化为json


85

建议namedtuple使用保留的字段名称将a序列化为json的建议方式是什么?

将anamedtuple序列化为json只会导致值序列化,并且字段名称在转换中丢失。我希望在对json大小化时也保留字段,因此请执行以下操作:

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

上面的代码按照我的预期序列化为json,并且namedtuple在我使用的其他地方(属性访问等)中起作用,除了在迭代过程中出现非元组之类的结果外(对于我的用例而言这很好)。

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


Answers:


56

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

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

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

这不是理想的方法,但是如果您只需要将namedtuple编码为字典,则另一种方法是将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"}}

12
从本质上讲,namedtuple非常适合JSON,因为它实际上是一个自定义类型,其键名在类型定义中是固定的,而字典中的键名存储在实例中。非常有见地的评论。我没有考虑过。谢谢。我喜欢namedtuple,因为它们提供了一个很好的不可变结构,并且方便属性命名。我会接受你的回答。话虽这么说,Java的序列化机制提供了对对象序列化方式的更多控制,我很好奇为什么Python中似乎不存在此类钩子。
calvinkrishy 2011年

那是我的第一种方法,但实际上并不起作用(无论如何对我来说)。
zeekay 2011年

1
>>> json.dumps(foobar('x', 'y'), cls=MyEncoder) <<< '["x", "y"]'
zeekay 2011年

19
啊,在python 2.7+中,_iterencode不再是JSONEncoder的方法。
zeekay 2011年

2
@calvin谢谢,我发现namedtuple也很有用,希望有更好的解决方案将其递归编码为JSON。@zeekay是的,似乎在2.7+中,他们将其隐藏起来,因此不能再被覆盖。真令人失望。
samplebias

77

如果只是一个 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}'

4
我收到AttributeError:在Windows上的Python 2.7(x64)中运行该代码时,“ FB”对象没有属性“ dict ”。但是fb._asdict()可以正常工作。
geographika

5
fb._asdict()vars(fb)会更好。
jpmc26 2015年

1
@ jpmc26:如果没有vars,则无法在对象上使用__dict__
Rufflewind

@Rufflewind您也不能__dict__在这些上使用。=)
jpmc26,2016年

4
在python 3__dict__已被删除。_asdict似乎对两者都起作用。
安迪·海登

21

看起来您曾经可以simplejson.JSONEncoder继承该类以使其工作,但是使用最新的simplejson代码,情况已不再如此:您必须实际修改项目代码。我看不出为什么simplejson不支持namedtuples的原因,所以我分叉了项目,添加了namedtuple支持,并且我目前正在等待将分支拉回到主项目中。如果您现在需要修复程序,只需从我的叉子上拉出即可。

编辑:看起来simplejson现在的最新版本本身带有该namedtuple_as_object选项,默认为True


3
您的编辑是正确的答案。simplejson对namedtuple的序列化与json不同(我认为:更好)。这确实使模式:“尝试:将simplejson作为json导入,除:import json”具有风险,因为根据是否安装了simplejson,在某些计算机上您可能会得到不同的行为。出于这个原因,我现在在我的许多安装文件中都要求使用simplejson,并放弃使用该模式。
marr75'9

1
@ marr75-同上ujson,在这种极端情况下,这更加离奇和不可预测...
mac

我能够使用以下方式获得序列化为(漂亮打印的)json的递归namedtuple:simplejson.dumps(my_tuple, indent=4)
KFL

5

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

它可以往返于命名元组并返回。

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

编辑:该库还支持dataclass和attr类。


2

它以递归方式将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
+1我的成绩差不多。但是您的回报是dict,而不是json。你必须有“不”,如果在你的对象的值是一个布尔值,它不会被转换为true,我认为它的安全改造成字典,然后使用json.dumps转换成JSON。
弗雷德·洛朗

2

还有一个更方便的解决方案是使用装饰器(它使用protected字段_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版本有几种情况。
LtWorf

是的,很明显。但是,未经测试,任何人都不应迁移到更新的Python版本。并且,其他解决方案使用_asdict,它也是“受保护的”类成员。
德米特里T.18年

1
LtWorf,您的图书馆是GPL,不适用于冻结集
Thomas Grainger

2
@LtWorf您的库也使用_fields;-) github.com/ltworf/typedload/blob/master/typedload/datadumper.py实际上, 它是namedtuple公共API的一部分:docs.python.org/3.7/library/…人们对此感到困惑下划线(难怪!)。这是糟糕的设计,但我不知道他们还有其他选择。
–quant_dev

1
什么东西?什么时候?您可以引用发行说明吗?
–quant_dev

2

jsonplus库提供了NamedTuple实例串行。如果需要,可使用其兼容模式输出简单对象,但最好使用默认值,因为它有助于回解码。


我在这里查看了其他解决方案,发现仅添加此依赖项可以节省很多时间。特别是因为我有一个NamedTuples列表,需要在会话中作为json传递。jsonplus使您基本上可以通过json配置名称元组的列表,.dumps().loads()无需进行配置就可以了。
Rob

0

这是一个老问题。然而:

对于所有有相同问题的人员的建议,请仔细考虑使用的任何私有或内部功能,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可以转换为列表。


3
_asdict是namedtuple公共API的一部分。他们解释了下划线docs.python.org/3.7/library/…的原因。 “除了从元组继承的方法外,命名元组还支持三个附加方法和两个属性。为防止与字段名冲突,方法和属性名从下划线开始。”
–quant_dev

@quant_dev谢谢,我没有看到这个解释。它不能保证api的稳定性,但可以使这些方法更值得信赖。我确实喜欢明确的to_dict可读性,但我可以看到它似乎在重新实现_as_dict
dlamblin

0

这是我对这个问题的看法。它序列化NamedTuple,照顾折叠后的NamedTuples和其中的List

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

0

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

最好使用更强大的库,例如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
}

0

simplejson.dump()而不是json.dump做这项工作。不过可能会慢一些。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.