将 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的主要内容,如果未能解决你的问题,请参考以下文章