在 Python 中删除列表中的重复字典

Posted

技术标签:

【中文标题】在 Python 中删除列表中的重复字典【英文标题】:Remove duplicate dict in list in Python 【发布时间】:2012-03-14 16:35:39 【问题描述】:

我有一个字典列表,我想删除具有相同键值对的字典。

对于这个列表:['a': 123, 'b': 123, 'a': 123]

我想退回这个:['a': 123, 'b': 123]

另一个例子:

对于这个列表:['a': 123, 'b': 1234, 'a': 3222, 'b': 1234, 'a': 123, 'b': 1234]

我想退回这个:['a': 123, 'b': 1234, 'a': 3222, 'b': 1234]

【问题讨论】:

你能告诉我们更多关于你试图解决的实际问题吗?这似乎是一个奇怪的问题。 我正在组合一些字典列表,并且有重复项。所以我需要删除那些重复项。 我在***.com/questions/480214/…的答案中找到了一个解决方案,没有使用set() @gfortune 我在现实生活中遇到了这个问题,其中有一个大型 ETL 脚本,该脚本将要上传的数据作为字典列表进行排队。有时,Scope A 中的多条记录会从 Scope B 中引入相同的记录,但无需将冗余输出上传到外部系统。 【参考方案1】:

试试这个:

[dict(t) for t in tuple(d.items()) for d in l]

策略是将字典列表转换为元组列表,其中元组包含字典项。由于可以对元组进行散列处理,您可以使用 set 删除重复项(在此处使用 set comprehension,较旧的 python 替代方案将是 set(tuple(d.items()) for d in l)),然后从元组重新创建字典dict

地点:

l是原始列表 d 是列表中的字典之一 t 是从字典创建的元组之一

编辑:如果您想保留排序,上面的单行将不起作用,因为set 不会这样做。但是,只需几行代码,您也可以做到这一点:

l = ['a': 123, 'b': 1234,
        'a': 3222, 'b': 1234,
        'a': 123, 'b': 1234]

seen = set()
new_l = []
for d in l:
    t = tuple(d.items())
    if t not in seen:
        seen.add(t)
        new_l.append(d)

print new_l

示例输出:

['a': 123, 'b': 1234, 'a': 3222, 'b': 1234]

注意:正如@a​​lexis 所指出的,具有相同键和值的两个字典可能不会产生相同的元组。如果他们经历不同的添加/删除密钥历史,则可能会发生这种情况。如果您的问题属于这种情况,请考虑按照他的建议对d.items() 进行排序。

【讨论】:

不错的解决方案,但它有一个错误:d.items() 不能保证以特定顺序返回元素。您应该执行tuple(sorted(d.items())) 以确保您不会为相同的键值对获得不同的元组。 @alexis 我做了一些测试,你确实是对的。如果在两者之间添加了很多键并稍后删除,那么可能就是这种情况。非常感谢您的评论。 注意,如果你像我一样从json 模块加载字典列表,这将不起作用 在这种情况下这是一个有效的解决方案,但在嵌套字典的情况下不起作用 它说“TypeError: unhashable type: 'list'”对于步骤“if t not in seen:”【参考方案2】:

另一个基于列表理解的单行:

>>> d = ['a': 123, 'b': 123, 'a': 123]
>>> [i for n, i in enumerate(d) if i not in d[n + 1:]]
['b': 123, 'a': 123]

这里因为我们可以使用dict比较,我们只保留不在初始列表其余部分的元素(这个概念只能通过索引n访问,因此使用enumerate)。

【讨论】:

这也适用于字典列表,其中包含与第一个答案相比的列表 这也适用于您的字典中可能有不可散列的类型作为值,这与最佳答案不同。 这里,目的是删除重复值,而不是键,看这个答案的代码 这是非常低效的代码。 if i not in d[n + 1:] 遍历整个字典列表(来自n,但这只是操作总数的一半)并且您正在检查字典中的每个元素,所以这段代码是 O(n^2) 时间复杂性 不适用于以字典为值的字典【参考方案3】:

如果使用第三方包没问题,那么你可以使用iteration_utilities.unique_everseen

>>> from iteration_utilities import unique_everseen
>>> l = ['a': 123, 'b': 123, 'a': 123]
>>> list(unique_everseen(l))
['a': 123, 'b': 123]

它保留了原始列表的顺序,并且 ut 还可以通过使用较慢的算法来处理字典等不可散列的项目(O(n*m) 其中n 是原始列表中的元素,m 是列表中的唯一元素原始列表而不是 O(n))。如果键和值都是可散列的,您可以使用该函数的 key 参数为“唯一性测试”创建可散列项(以便它在 O(n) 中工作)。

对于字典(独立于顺序进行比较),您需要将其映射到另一个类似这样比较的数据结构,例如frozenset

>>> list(unique_everseen(l, key=lambda item: frozenset(item.items())))
['a': 123, 'b': 123]

请注意,您不应该使用简单的 tuple 方法(没有排序),因为相等的字典不一定具有相同的顺序(即使在 Python 3.7 中 插入顺序 - 不是绝对顺序- 保证):

>>> d1 = 1: 1, 9: 9
>>> d2 = 9: 9, 1: 1
>>> d1 == d2
True
>>> tuple(d1.items()) == tuple(d2.items())
False

如果键不可排序,甚至对元组进行排序也可能不起作用:

>>> d3 = 1: 1, 'a': 'a'
>>> tuple(sorted(d3.items()))
TypeError: '<' not supported between instances of 'str' and 'int'

基准测试

我认为比较这些方法的性能可能会有用,因此我做了一个小型基准测试。基准图是基于不包含重复项的列表的时间与列表大小(这是任意选择的,如果我添加一些或大量重复项,运行时不会发生显着变化)。这是一个对数图,因此涵盖了整个范围。

绝对次数:

相对于最快方法的时间:

thefourtheye 的第二种方法在这里最快。带有key 函数的unique_everseen 方法排在第二位,但它是保持顺序的最快方法。 jcollado 和 thefourtheye 的其他方法几乎一样快。使用不带密钥的unique_everseen 的方法以及来自Emmanuel 和Scorpil 的解决方案对于较长的列表非常慢,并且表现得更差O(n*n) 而不是O(n)。 stpks 与 json 的方法不是 O(n*n),但它比类似的 O(n) 方法慢得多。

重现基准的代码:

from simple_benchmark import benchmark
import json
from collections import OrderedDict
from iteration_utilities import unique_everseen

def jcollado_1(l):
    return [dict(t) for t in tuple(d.items()) for d in l]

def jcollado_2(l):
    seen = set()
    new_l = []
    for d in l:
        t = tuple(d.items())
        if t not in seen:
            seen.add(t)
            new_l.append(d)
    return new_l

def Emmanuel(d):
    return [i for n, i in enumerate(d) if i not in d[n + 1:]]

def Scorpil(a):
    b = []
    for i in range(0, len(a)):
        if a[i] not in a[i+1:]:
            b.append(a[i])

def stpk(X):
    set_of_jsons = json.dumps(d, sort_keys=True) for d in X
    return [json.loads(t) for t in set_of_jsons]

def thefourtheye_1(data):
    return OrderedDict((frozenset(item.items()),item) for item in data).values()

def thefourtheye_2(data):
    return frozenset(item.items()):item for item in data.values()

def iu_1(l):
    return list(unique_everseen(l))

def iu_2(l):
    return list(unique_everseen(l, key=lambda inner_dict: frozenset(inner_dict.items())))

funcs = (jcollado_1, Emmanuel, stpk, Scorpil, thefourtheye_1, thefourtheye_2, iu_1, jcollado_2, iu_2)
arguments = 2**i: ['a': j for j in range(2**i)] for i in range(2, 12)
b = benchmark(funcs, arguments, 'list size')

%matplotlib widget
import matplotlib as mpl
import matplotlib.pyplot as plt
plt.style.use('ggplot')
mpl.rcParams['figure.figsize'] = '8, 6'

b.plot(relative_to=thefourtheye_2)

为了完整起见,这里是仅包含重复项的列表的时间:

# this is the only change for the benchmark
arguments = 2**i: ['a': 1 for j in range(2**i)] for i in range(2, 12)

除了没有key 函数的unique_everseen 之外,时间没有显着变化,在这种情况下这是最快的解决方案。然而,这只是具有不可散列值的该函数的最佳情况(因此不具有代表性),因为它的运行时间取决于列表中唯一值的数量:O(n*m),在这种情况下仅为 1,因此它在O(n) 中运行。


免责声明:我是iteration_utilities的作者。

【讨论】:

【参考方案4】:

如果您对嵌套字典(例如反序列化 JSON 对象)进行操作,其他答案将不起作用。对于这种情况,您可以使用:

import json
set_of_jsons = json.dumps(d, sort_keys=True) for d in X
X = [json.loads(t) for t in set_of_jsons]

【讨论】:

太棒了!诀窍是dict对象不能直接添加到集合中,需要通过dump()将其转换为json对象。【参考方案5】:

有时旧式循环仍然有用。这段代码比 jcollado 的略长,但非常容易阅读:

a = ['a': 123, 'b': 123, 'a': 123]
b = []
for i in range(0, len(a)):
    if a[i] not in a[i+1:]:
        b.append(a[i])

【讨论】:

0in range(0, len(a)) 不是必需的。【参考方案6】:

如果您在工作流程中使用 Pandas,一种选择是将字典列表直接提供给 pd.DataFrame 构造函数。然后使用drop_duplicatesto_dict 方法获得所需的结果。

import pandas as pd

d = ['a': 123, 'b': 1234, 'a': 3222, 'b': 1234, 'a': 123, 'b': 1234]

d_unique = pd.DataFrame(d).drop_duplicates().to_dict('records')

print(d_unique)

['a': 123, 'b': 1234, 'a': 3222, 'b': 1234]

【讨论】:

【参考方案7】:

如果您想保留订单,那么您可以这样做

from collections import OrderedDict
print OrderedDict((frozenset(item.items()),item) for item in data).values()
# ['a': 123, 'b': 1234, 'a': 3222, 'b': 1234]

如果顺序无关紧要,那么你可以这样做

print frozenset(item.items()):item for item in data.values()
# ['a': 3222, 'b': 1234, 'a': 123, 'b': 1234]

【讨论】:

注意:在 python 3 中,您的第二种方法提供了不可序列化的 dict_values 输出而不是列表。您必须再次将整个内容放入列表中。 list(frozen.....)【参考方案8】:

不是一个通用的答案,但如果您的列表恰好按某个键排序,如下所示:

l=['a': 'b': 31, 't': 1,
   'a': 'b': 31, 't': 1,
 'a': 'b': 145, 't': 2,
 'a': 'b': 25231, 't': 2,
 'a': 'b': 25231, 't': 2, 
 'a': 'b': 25231, 't': 2, 
 'a': 'b': 112, 't': 3]

那么解决方法就这么简单:

import itertools
result = [a[0] for a in itertools.groupby(l)]

结果:

['a': 'b': 31, 't': 1,
'a': 'b': 145, 't': 2,
'a': 'b': 25231, 't': 2,
'a': 'b': 112, 't': 3]

适用于嵌套字典并(显然)保持顺序。

【讨论】:

这甚至适用于带有列表的字典。【参考方案9】:

你可以使用一个集合,但是你需要把字典变成一个可散列的类型。

seq = ['a': 123, 'b': 1234, 'a': 3222, 'b': 1234, 'a': 123, 'b': 1234]
unique = set()
for d in seq:
    t = tuple(d.iteritems())
    unique.add(t)

现在唯一等于

set([(('a', 3222), ('b', 1234)), (('a', 123), ('b', 1234))])

要恢复字典:

[dict(x) for x in unique]

【讨论】:

d.iteritems() 的顺序无法保证 - 因此您可能会在 unique 中出现“重复”。【参考方案10】:

最简单的方法是将列表中的每个项目转换为字符串,因为字典不可散列。然后您可以使用 set 删除重复项。

list_org = ['a': 123, 'b': 123, 'a': 123]
list_org_updated = [ str(item) for item in list_org]
print(list_org_updated)
["'a': 123", "'b': 123", "'a': 123"]
unique_set = set(list_org_updated)
print(unique_set)
"'b': 123", "'a': 123"

您可以使用该集合,但如果您确实想要一个列表,请添加以下内容:

import ast
unique_list = [ast.literal_eval(item) for item in unique_set]
print(unique_list)
['b': 123, 'a': 123]

【讨论】:

【参考方案11】:

通过自定义键删除重复项:

def remove_duplications(arr, key):
    return list(key(x): x for x in arr.values())

【讨论】:

【参考方案12】:

这是一个具有双重嵌套列表理解的快速单行解决方案(基于 @Emmanuel 的解决方案)。

这使用每个字典中的单个键(例如,a)作为主键,而不是检查整个字典是否匹配

[i for n, i in enumerate(list_of_dicts) if i.get(primary_key) not in [y.get(primary_key) for y in list_of_dicts[n + 1:]]]

这不是 OP 要求的,但正是它把我带到了这个帖子,所以我想我会发布我最终得到的解决方案

【讨论】:

【参考方案13】:

很多搜索重复值和键的好例子,下面是我们过滤列表中整个字典重复数据的方法。如果您的源数据由 EXACT 格式的字典组成并查找重复项,请使用 dupKeys = []。否则将 dupKeys = 设置为您希望没有重复条目的数据的键名,可以是 1 到 n 个键。它不是优雅的,但可以工作并且非常灵活

import binascii

collected_sensor_data = ["sensor_id":"nw-180","data":"XXXXXXX",
                         "sensor_id":"nw-163","data":"ZYZYZYY",
                         "sensor_id":"nw-180","data":"XXXXXXX",
                         "sensor_id":"nw-97", "data":"QQQQQZZ"]

dupKeys = ["sensor_id", "data"]

def RemoveDuplicateDictData(collected_sensor_data, dupKeys):

    checkCRCs = []
    final_sensor_data = []
    
    if dupKeys == []:
        for sensor_read in collected_sensor_data:
            ck1 = binascii.crc32(str(sensor_read).encode('utf8'))
            if not ck1 in checkCRCs:
                final_sensor_data.append(sensor_read)
                checkCRCs.append(ck1)
    else:
        for sensor_read in collected_sensor_data:
            tmp = ""
            for k in dupKeys:
                tmp += str(sensor_read[k])

            ck1 = binascii.crc32(tmp.encode('utf8'))
            if not ck1 in checkCRCs:
                final_sensor_data.append(sensor_read)
                checkCRCs.append(ck1)
  
           
    return final_sensor_data    

 final_sensor_data = ["sensor_id":"nw-180","data":"XXXXXXX",
                      "sensor_id":"nw-163","data":"ZYZYZYY",
                      "sensor_id":"nw-97", "data":"QQQQQZZ"]
    

【讨论】:

【参考方案14】:

不是那么短,但很容易阅读:

list_of_data = ['a': 123, 'b': 123, 'a': 123]

list_of_data_uniq = []
for data in list_of_data:
    if data not in list_of_data_uniq:
        list_of_data_uniq.append(data)

现在,列表list_of_data_uniq 将具有唯一的字典。

【讨论】:

以上是关于在 Python 中删除列表中的重复字典的主要内容,如果未能解决你的问题,请参考以下文章

使用Apache-beam在Python中删除字典中的第一项[重复]

从Python中的字典中删除键[重复]

python中怎么重复打印

python中的joinset集合深浅拷贝

python删除列表中的重复元素并保持相对顺序不变

Python中的列表,元祖,集合,字典