将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个回答

103

如果您只想序列化一个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
在Windows上以Python 2.7(x64)运行该代码时,我收到了AttributeError:'FB'对象没有属性'dict'的错误。但是,fb._asdict()可以正常工作。 - geographika
8
fb._asdict()vars(fb) 更好。 - jpmc26
1
@jpmc26: 在没有__dict__的对象上无法使用vars - Rufflewind
是的,你可以使用__dict__,它被定义为"__dict__ = _property(_asdict)",请参见标准库中的collections.py。 - benselme
4
在Python 3中,__dict__已被移除。_asdict似乎在两个版本上都能正常工作。 - Andy Hayden
显示剩余4条评论

60

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

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

从本质上讲,命名元组(namedtuple)与JSON不太匹配,因为它实际上是一个定制类型,其键名在类型定义中被固定,而字典的键名则存储在实例内部。这使得你无法对一个命名元组进行"往返"操作。例如,你不能将一个字典解码回一个命名元组,除非字典中包含其他信息,比如应用特定的类型标记{'a': 1, '#_type': 'foobar'},这有点麻烦。

虽然这种情况并不理想,但如果你只需要将命名元组编码为字典,另一种方法是扩展或修改你的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"}}

17
namedtuple作为一个自定义类型,它的键名在类型定义中是固定的,这使它不太适合用于JSON。相比之下,字典的键名存储在实例内部,更符合JSON的特点。非常有见地的评论。我之前没有考虑到这一点,谢谢。我喜欢namedtuple,因为它提供了一个带有属性命名便利的漂亮的不可变结构体。我会接受你的答案。话虽如此,在Java中,序列化机制提供了更多关于对象序列化方式的控制,并且我很好奇为什么Python似乎不存在这样的钩子函数。 - calvinkrishy
1
那是我的第一种方法,但实际上它并不起作用(至少对我来说是这样)。 - Zach Kelling
2
>>> json.dumps(foobar('x', 'y'), cls=MyEncoder) <<< '["x", "y"]' - Zach Kelling
23
在Python 2.7及以上版本中,_iterencode不再是JSONEncoder的一个方法。 - Zach Kelling
2
@calvin 谢谢,我也觉得namedtuple很有用,但希望能有更好的解决方案来递归地将其编码为JSON。 @zeekay 是的,在2.7+中它们隐藏了它,所以它不能再被覆盖。这令人失望。 - samplebias
显示剩余2条评论

22
看起来您过去可以通过子类化simplejson.JSONEncoder来使其工作,但是最新的simplejson代码不再支持此操作:您必须实际修改项目代码。我认为simplejson应该支持namedtuple,所以我fork了这个项目,添加了namedtuple支持,现在我正在等待我的分支被合并到主项目中。如果您需要立即修复,请从我的fork中拉取。 编辑:看起来最新版本的simplejson现在使用namedtuple_as_object选项本地支持这一点,该选项默认为True

3
你的编辑是正确的答案。simplejson 对命名元组进行序列化时(我的观点是更好),与 json 有所不同。这确实让这种模式:" 尝试导入 simplejson as json,如果失败就导入 json" 非常冒险,因为在某些机器上,如果安装了 simplejson,则可能会得到不同的行为。因此,出于这个原因,我现在在很多设置文件中都要求使用 simplejson,并避免使用那种模式。 - marr75
2
@marr75 - 对于 ujson 也一样,它在这种边缘情况下甚至更加奇怪和不可预测... - mac
我成功地使用以下代码将递归的namedtuple序列化为(漂亮打印的)json:simplejson.dumps(my_tuple, indent=4) - KFL

6
我写了一个库来完成这个: https://github.com/ltworf/typedload 它可以实现从和到命名元组的转换。
它支持相当复杂的嵌套结构,包括列表、集合、枚举、联合、默认值。它应该可以涵盖大多数常见情况。
编辑:该库还支持数据类和attr类。

5

有一种更方便的解决方案是使用装饰器(它使用受保护字段 _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))

是的,这很清楚。然而,没有人应该在没有测试的情况下迁移到新版本的Python。另外,其他解决方案使用了_asdict,这也是一个“受保护”的类成员。 - Dmitry T.
1
LtWorf,你的库是GPL许可的,但不支持frozensets。 - Thomas Grainger
2
@LtWorf 你的库也使用了 _fields ;-) https://github.com/ltworf/typedload/blob/master/typedload/datadumper.py实际上,这是namedtuple的公共API之一:https://docs.python.org/3.7/library/collections.html#collections.namedtuple 人们会因为下划线而感到困惑(难怪!)。这是糟糕的设计,但我不知道他们还有什么其他选择。 - quant_dev
_fields 是什么时候被改变的? - quant_dev
1
什么事情?什么时候?你能引用发布说明吗? - quant_dev
显示剩余7条评论

5

使用原生的Python JSON库对命名元组进行序列化是不可能正确进行的。 这个库总是把命名元组当作列表来看待,而且不可能覆盖默认序列化器以更改此行为。 如果对象嵌套,情况会变得更加糟糕。

最好使用一个更健壮的库,例如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
}

1
我也是orjson的粉丝。 - CircleOnCircles
1
orjson很棒(还可以处理日期、数据类等),但在某些罕见的架构上(例如我在termux上使用它时),由于它是基于Rust的,它无法构建。因此,如果可移植性是一个问题,simplejson可能更好。也可以尝试导入orjson以获得更好的性能,否则如果出现ModuleNotFound错误,则回退到simplejson(因为那是纯Python)- 示例 - Sean Breckenridge

3

它递归地将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'}

2
+1 我做的几乎一样。但是你的返回值是字典而不是JSON。你必须使用双引号而不是单引号,如果对象中的值是布尔值,则不会转换为true。我认为更安全的方法是将其转换为字典,然后使用json.dumps将其转换为JSON。 - Fred Laurent

2

jsonplus库提供了一个NamedTuple实例的序列化器。如果需要输出简单对象,请使用其兼容模式,但最好使用默认模式,因为它有助于解码回来。


我看了这里的其他解决方案,发现仅添加此依赖项就为我节省了很多时间。特别是因为我有一个NamedTuples列表需要作为json在会话中传递。 jsonplus可以让你基本上使用.dumps().loads()将命名元组列表转换为json,并从json中获取,没有配置,它只是起作用。 - Rob

1

simplejson.dump() 而不是 json.dump 可以完成任务。但它可能会慢一些。


1
这是我的解决方案。它将NamedTuple序列化,并处理其中包含的折叠NamedTuples和列表。
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

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