给定一个字典列表,如何消除一个键的重复,并按另一个键排序

11

我正在使用一个由 dict 对象组成的 list,它看起来像这样(对象的顺序不同):

[
    {'name': 'Foo', 'score': 1},
    {'name': 'Bar', 'score': 2},
    {'name': 'Foo', 'score': 3},
    {'name': 'Bar', 'score': 3},
    {'name': 'Foo', 'score': 2},
    {'name': 'Baz', 'score': 2},
    {'name': 'Baz', 'score': 1},
    {'name': 'Bar', 'score': 1}
]

我想要做的是删除重复的名称,仅保留每个名称中得分最高的一个。来自上述列表的结果如下:

[
    {'name': 'Baz', 'score': 2},
    {'name': 'Foo', 'score': 3},
    {'name': 'Bar', 'score': 3}
]

我不确定在这里应该使用哪种模式(除了一个看起来很蠢的循环,它不断检查当前dict'name'是否已经在列表中,然后检查它的'score'是否高于现有的那个'score')。


3
跟着循环走,它简单明了。 - Robert Peters
3
六个月后如果你需要“稍微”更改它,那么这篇文章很简单、清晰并且易于阅读。 - James Khoury
2
+1 这个问题有一些神奇的地方,因为它引发了各种不同和有趣的答案。这个问题有多少完全不同的解决方案是很有趣的。我将其标记为收藏夹,因为它有丰富的答案集(同时也会给每个具有创意或有趣解决方案的答案点赞)。 - Raymond Hettinger
1
@Raymond - 谢谢。我也很兴奋看到这么多的选择。我有几个版本,但从列表中得到了一个更干净的版本。顺便说一下,我拥有并阅读了你的书(《高级Python》,如果你还有其他书),它非常棒。我个人认为你应该再写一本书,也许是关于更高级的Python或Python设计模式的书。 - orokusaki
7个回答

15

一种方法是:

data = collections.defaultdict(list)
for i in my_list:
    data[i['name']].append(i['score'])
output = [{'name': i, 'score': max(j)} for i,j in data.items()]

因此输出将是:

[{'score': 2, 'name': 'Baz'},
 {'score': 3, 'name': 'Foo'},
 {'score': 3, 'name': 'Bar'}]

10

这里不需要使用defaultdict或set。您可以使用极简单的字典和列表。

在字典中汇总最佳得分,并将结果转换回列表:

>>> s = [
    {'name': 'Foo', 'score': 1},
    {'name': 'Bar', 'score': 2},
    {'name': 'Foo', 'score': 3},
    {'name': 'Bar', 'score': 3},
    {'name': 'Foo', 'score': 2},
    {'name': 'Baz', 'score': 2},
    {'name': 'Baz', 'score': 1},
    {'name': 'Bar', 'score': 1}
]
>>> d = {}
>>> for entry in s:
        name, score = entry['name'], entry['score']
        d[name] = max(d.get(name, 0), score)

>>> [{'name': name, 'score': score} for name, score in d.items()]
[{'score': 2, 'name': 'Baz'}, {'score': 3, 'name': 'Foo'}, {'score': 3, 'name': 'Bar'}]

1
如果我们使用像{'Foo': 3}这样的数据结构而不是[{'name': 'Foo', 'score': 3}],那么这个解决方案将会是最优雅的。我认为原问题的发布者应该这样做。 - fletom
1
这是我最喜欢的解决方案。唯一需要改变的是将 d.get(name,0) 改为 d.get(name,score)。这样可以允许负分数。 - Rusty Rob

4

仅供娱乐,这里是一种纯函数式的方法:

>>> map(dict, dict(sorted(map(sorted, map(dict.items, s)))).items())
[{'score': 3, 'name': 'Bar'}, {'score': 2, 'name': 'Baz'}, {'score': 3, 'name': 'Foo'}]

3

排序是解决问题的一半。

import itertools
import operator

scores = [
    {'name': 'Foo', 'score': 1},
    {'name': 'Bar', 'score': 2},
    {'name': 'Foo', 'score': 3},
    {'name': 'Bar', 'score': 3},
    {'name': 'Foo', 'score': 2},
    {'name': 'Baz', 'score': 2},
    {'name': 'Baz', 'score': 1},
    {'name': 'Bar', 'score': 1}
]

result = []
sl = sorted(scores, key=operator.itemgetter('name', 'score'),
  reverse=True)
name = object()
for el in sl:
  if el['name'] == name:
    continue
  name = el['name']
  result.append(el)
print result

1
+1 这个答案是唯一一个不改变数据集的答案。看起来很一致,如果 OP 想要,字典可以有额外的项。 - JBernardo
在这里使用 object() 的目的是什么? - fletom
2
@nomulous:它创建了一个在字典中不可能找到的新对象。None(或者其他任何已存在的对象)可以(虽然在这种情况下并没有)在数据中被找到。 - Ignacio Vazquez-Abrams
@Ignacio 太棒了!我刚刚输入了 object() == object(),答案是 False,非常方便。谢谢! - fletom

2
这是我能想到的最简单的方式:
names = set(d['name'] for d in my_dicts)
new_dicts = []
for name in names:
    d = dict(name=name)
    d['score'] = max(d['score'] for d in my_dicts if d['name']==name)
    new_dicts.append(d)

#new_dicts
[{'score': 2, 'name': 'Baz'},
 {'score': 3, 'name': 'Foo'},
 {'score': 3, 'name': 'Bar'}]

个人而言,当问题较小时,我更倾向于不导入模块。


2

如果您还没有听说过group by,这是它的一个很好的用法:

from itertools import groupby

data=[
    {'name': 'Foo', 'score': 1},
    {'name': 'Bar', 'score': 2},
    {'name': 'Foo', 'score': 3},
    {'name': 'Bar', 'score': 3},
    {'name': 'Foo', 'score': 2},
    {'name': 'Baz', 'score': 2},
    {'name': 'Baz', 'score': 1},
    {'name': 'Bar', 'score': 1}
]

keyfunc=lambda d:d['name']
data.sort(key=keyfunc)

ans=[]
for k, g in groupby(data, keyfunc):
    ans.append({k:max((d['score'] for d in g))})
print ans

>>>
[{'Bar': 3}, {'Baz': 2}, {'Foo': 3}]

2

我认为我可以在这里提出一句话:

result = dict((x['name'],x) for x in sorted(data,key=lambda x: x['score'])).values()

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