将 Python namedtuple 序列化为 json

Posted

技术标签:

【中文标题】将 Python namedtuple 序列化为 json【英文标题】:Serializing a Python namedtuple to 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 2.7:***.com/questions/16938456/… 【参考方案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 完成这项工作。不过可能会慢一些。

【讨论】:

以上是关于将 Python namedtuple 序列化为 json的主要内容,如果未能解决你的问题,请参考以下文章

将 Python 字典序列化为 XML [关闭]

了解 JSONEncoder 的子类化

无法将 python 列表序列化为 json

将json字符串反序列化为python中的对象

将 C++ 类序列化为文件,然后在 Python 中进行基于事件的反序列​​化?

如何将 django/python 中的模型对象列表序列化为 JSON