我知道这不是您要求的折叠式解决方案,但我会这样做itertools
,它同样具有功能性(除非你认为 Haskell 的功能性不如 Lisp……),而且也可能是解决这个问题的最 Pythonic 方法。
这个想法是将您的序列视为一个惰性列表,并对它应用一系列惰性转换,直到获得所需的列表。
这里的关键步骤是groupby http://docs.python.org/2/library/itertools.html#itertools.groupby:
>>> initial = json.loads(s)
>>> groups = itertools.groupby(initial, operator.itemgetter('query'))
>>> print([key, list(group) for key, group in groups])
[('Q1',
[{'detail': 'cool', 'query': 'Q1', 'rank': 1, 'url': 'awesome1'},
{'detail': 'cool', 'query': 'Q1', 'rank': 2, 'url': 'awesome2'},
{'detail': 'cool', 'query': 'Q1', 'rank': 3, 'url': 'awesome3'}]),
('Q#2',
[{'detail': 'same', 'query': 'Q#2', 'rank': 1, 'url': 'newurl1'},
{'detail': 'same', 'query': 'Q#2', 'rank': 2, 'url': 'newurl2'},
{'detail': 'same', 'query': 'Q#2', 'rank': 3, 'url': 'newurl3'}])]
只需一步,您就可以看到我们已经有多接近了。
要重组每个键,请将对分组为您想要的字典格式:
>>> groups = itertools.groupby(initial, operator.itemgetter('query'))
>>> print([{"query": key, "results": list(group)} for key, group in groups])
[{'query': 'Q1',
'results': [{'detail': 'cool',
'query': 'Q1',
'rank': 1,
'url': 'awesome1'},
{'detail': 'cool',
'query': 'Q1',
'rank': 2,
'url': 'awesome2'},
{'detail': 'cool',
'query': 'Q1',
'rank': 3,
'url': 'awesome3'}]},
{'query': 'Q#2',
'results': [{'detail': 'same',
'query': 'Q#2',
'rank': 1,
'url': 'newurl1'},
{'detail': 'same',
'query': 'Q#2',
'rank': 2,
'url': 'newurl2'},
{'detail': 'same',
'query': 'Q#2',
'rank': 3,
'url': 'newurl3'}]}]
但是等等,您仍然需要删除那些额外的字段。简单的:
>>> groups = itertools.groupby(initial, operator.itemgetter('query'))
>>> def filterkeys(d):
... return {k: v for k, v in d.items() if k in ('rank', 'url')}
>>> filtered = ((key, map(filterkeys, group)) for key, group in groups)
>>> print([{"query": key, "results": list(group)} for key, group in filtered])
[{'query': 'Q1',
'results': [{'rank': 1, 'url': 'awesome1'},
{'rank': 2, 'url': 'awesome2'},
{'rank': 3, 'url': 'awesome3'}]},
{'query': 'Q#2',
'results': [{'rank': 1, 'url': 'newurl1'},
{'rank': 2, 'url': 'newurl2'},
{'rank': 3, 'url': 'newurl3'}]}]
唯一剩下要做的就是打电话json.dumps
代替print
.
对于您的后续操作,您希望采用具有相同值的每一行中相同的所有值query
并将它们分组为otherstuff
,然后列出剩余的内容results
.
因此,对于每个组,首先我们想要获得公共密钥。我们可以通过迭代组中任何成员的键来做到这一点(任何不在第一个成员中的东西不能在所有成员中),所以:
def common_fields(group):
def in_all_members(key, value):
return all(member[key] == value for member in group[1:])
return {key: value for key, value in group[0].items() if in_all_members(key, value)}
或者,或者……如果我们把每个成员变成set
键值对,而不是字典,我们可以intersect
商场。这意味着我们终于可以使用reduce
,所以让我们尝试一下:
def common_fields(group):
return dict(functools.reduce(set.intersection, (set(d.items()) for d in group)))
我认为之间的来回转换dict
and set
可能会降低可读性,并且这也意味着您的值必须是可散列的(对于您的示例数据来说这不是问题,因为这些值都是字符串)......但它肯定更简洁。
当然,这将始终包括query
作为一个公共领域,但我们稍后会处理这个问题。 (另外,你想要otherstuff
成为一个list
与一个dict
,所以我们会在它周围添加一对额外的括号)。
同时,results
与上面相同,除了filterkeys
过滤掉所有公共字段,而不是过滤掉除此之外的所有字段rank
and url
。把它放在一起:
def process_group(group):
group = list(group)
common = dict(functools.reduce(set.intersection, (set(d.items()) for d in group)))
def filterkeys(member):
return {k: v for k, v in member.items() if k not in common}
results = list(map(filterkeys, group))
query = common.pop('query')
return {'query': query,
'otherstuff': [common],
'results': list(results)}
所以,现在我们只使用该函数:
>>> groups = itertools.groupby(initial, operator.itemgetter('query'))
>>> print([process_group(group) for key, group in groups])
[{'otherstuff': [{'detail': 'cool'}],
'query': 'Q1',
'results': [{'rank': 1, 'url': 'awesome1'},
{'rank': 2, 'url': 'awesome2'},
{'rank': 3, 'url': 'awesome3'}]},
{'otherstuff': [{'detail': 'same'}],
'query': 'Q#2',
'results': [{'rank': 1, 'url': 'newurl1'},
{'rank': 2, 'url': 'newurl2'},
{'rank': 3, 'url': 'newurl3'}]}]
这显然不像原始版本那么简单,但希望这一切仍然有意义。只有两个新技巧。首先,我们必须迭代groups
多次(一次查找公共密钥,然后再次提取剩余密钥)