使用Python >= 2.7将嵌套的命名元组序列化为JSON

13

我遇到了一个类似于CalvinKrishy的问题

Samplebias的解决方案在我手上的数据上不起作用。

我正在使用Python 2.7。

这是我的数据:

命名元组

>>> a_t = namedtuple('a','f1 words')
>>> word_t = namedtuple('word','f2 value')
>>> w1 = word_t(f2=[0,1,2], value='abc')
>>> w2 = word_t(f2=[3,4], value='def')
>>> a1 = a_t(f1=[0,1,2,3,4],words=[w1, w2])
>>> a1
a(f1=[0, 1, 2, 3, 4], words=[word(f2=[0, 1, 2], value='abc'), word(f2=[3, 4], value='def')])

字典

>>> w3 = {}
>>> w3['f2'] = [0,1,2]
>>> w3['value'] = 'abc'
>>> w4 = {}
>>> w4['f2'] = [3,4]
>>> w4['value'] = 'def'
>>> a2 = {}
>>> a2['f1'] = [0, 1, 2, 3, 4]
>>> a2['words'] = [w3,w4]
>>> a2
{'f1': [0, 1, 2, 3, 4], 'words': [{'f2': [0, 1, 2], 'value': 'abc'}, {'f2': [3, 4], 'value': 'def'}]}

您可以看到a1和a2都是一样的,除了一个是命名元组而另一个是字典

但是json.dumps却不同:

>>> json.dumps(a1._asdict())
'{"f1": [0, 1, 2, 3, 4], "words": [[[0, 1, 2], "abc"], [[3, 4], "def"]]}'
>>> json.dumps(a2)
'{"f1": [0, 1, 2, 3, 4], "words": [{"f2": [0, 1, 2], "value": "abc"}, {"f2": [3, 4], "value": "def"}]}'

我希望以与a2相同的方式获取a1的json格式。


4
命名元组是元组的子类,因此JSON将其序列化为列表,这是正确的做法。 - Martijn Pieters
但是http://docs.python.org/dev/library/collections.html#collections.somenamedtuple._asdict返回一个有序字典。 - Kaushik Acharya
1
这是一个实用方法;在将元组序列化为JSON之前,先将其转换为有序字典。 - Martijn Pieters
1
@MartijnPieters 在这种情况下,您无法覆盖json编码器的行为,因为它将命名元组视为元组。 - Emil
我建议使用装饰器 - Dmitry T.
显示剩余2条评论
2个回答

13

问题在于使用namedtuple._asdict,而不是json.dumps。如果你使用namedtuple(..., verbose=True)查看代码,你会看到这个:

def _asdict(self):
    'Return a new OrderedDict which maps field names to their values'
    return OrderedDict(zip(self._fields, self))

只有顶层被改变为OrderedDict,所有包含的元素都不会被改变。这意味着嵌套的namedtuple仍然是tuple子类并且可以(正确地)序列化等操作。

如果对特定转换函数的调用对您来说是可接受的(例如对_asdict的调用),则可以编写自己的函数。

def namedtuple_asdict(obj):
  if hasattr(obj, "_asdict"): # detect namedtuple
    return OrderedDict(zip(obj._fields, (namedtuple_asdict(item) for item in obj)))
  elif isinstance(obj, basestring): # iterables - strings
     return obj
  elif hasattr(obj, "keys"): # iterables - mapping
     return OrderedDict(zip(obj.keys(), (namedtuple_asdict(item) for item in obj.values())))
  elif hasattr(obj, "__iter__"): # iterables - sequence
     return type(obj)((namedtuple_asdict(item) for item in obj))
  else: # non-iterable cannot contain namedtuples
    return obj

json.dumps(namedtuple_asdict(a1))
# prints '{"f1": [0, 1, 2, 3, 4], "words": [{"f2": [0, 1, 2], "value": "abc"}, {"f2": [3, 4], "value": "def"}]}'

如您所见,最大的问题在于嵌套结构并非namedtuple,但可能包含它们。


1
只是提醒一下:此函数对于任何循环数据结构都不安全。 - MisterMiyagi
3
在这段代码中,将Python 3中的basestring替换为str - Amit Kotlovski

2

这里是我选择的版本,改编自MisterMiyagi的版本。我使用了collections.abc中的isinstance而不是hasattr,并在生成的字典中包含一个名为_type的键,其值为namedtuple类的名称。

import collections.abc

def _nt_to_dict(obj):
    recurse = lambda x: map(_nt_to_dict, x)
    obj_is = lambda x: isinstance(obj, x)
    if obj_is(tuple) and hasattr(obj, '_fields'):  # namedtuple
        fields = zip(obj._fields, recurse(obj))
        class_name = obj.__class__.__name__
        return dict(fields, **{'_type': class_name})
    elif obj_is(collections.abc.Mapping):
        return type(obj)(zip(obj.keys(), recurse(obj.values())))
    elif obj_is(collections.abc.Iterable) and not obj_is(str):
        return type(obj)(recurse(obj))
    else:
        return obj

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