Xóa dict trùng lặp trong danh sách trong Python


153

Tôi có một danh sách các dicts, và tôi muốn loại bỏ các dicts với các cặp khóa và giá trị giống hệt nhau.

Đối với danh sách này: [{'a': 123}, {'b': 123}, {'a': 123}]

Tôi muốn trả lại cái này: [{'a': 123}, {'b': 123}]

Một vi dụ khac:

Đối với danh sách này: [{'a': 123, 'b': 1234}, {'a': 3222, 'b': 1234}, {'a': 123, 'b': 1234}]

Tôi muốn trả lại cái này: [{'a': 123, 'b': 1234}, {'a': 3222, 'b': 1234}]


Bạn có thể cho chúng tôi biết thêm về vấn đề thực tế mà bạn đang cố gắng giải quyết không? Điều này có vẻ như một vấn đề kỳ lạ để có.
gfortune

Tôi đang kết hợp một vài danh sách các dicts và có những bản sao. Vì vậy, tôi cần phải loại bỏ những bản sao đó.
Brenden

Tôi đã tìm thấy một giải pháp trong stackoverflow.com/questions/480214/ trong một câu trả lời mà không cần sử dụngset()
Sebastian Wagner

Câu trả lời:


242

Thử cái này:

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

Chiến lược là chuyển đổi danh sách các từ điển thành một danh sách các bộ dữ liệu trong đó các bộ dữ liệu chứa các mục của từ điển. Vì các bộ dữ liệu có thể được băm, bạn có thể loại bỏ các bản sao bằng cách sử dụng set(sử dụng cách hiểu tập hợp ở đây, thay thế python cũ hơn set(tuple(d.items()) for d in l)) và sau đó, tạo lại từ điển từ bộ dữ liệu với dict.

Ở đâu:

  • l là danh sách ban đầu
  • d là một trong những từ điển trong danh sách
  • t là một trong những bộ dữ liệu được tạo từ một từ điển

Chỉnh sửa: Nếu bạn muốn duy trì trật tự, lớp lót ở trên sẽ không hoạt động vì setsẽ không làm điều đó. Tuy nhiên, với một vài dòng mã, bạn cũng có thể làm điều đó:

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

Ví dụ đầu ra:

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

Lưu ý: Như được chỉ ra bởi @alexis, có thể xảy ra việc hai từ điển có cùng khóa và giá trị, không dẫn đến cùng một bộ dữ liệu. Điều đó có thể xảy ra nếu họ trải qua lịch sử thêm / xóa khóa khác. Nếu đó là trường hợp cho vấn đề của bạn, thì hãy xem xét sắp xếp d.items()như anh ấy gợi ý.


35
Giải pháp hay nhưng có một lỗi: d.items()không được đảm bảo trả về các phần tử theo một thứ tự cụ thể. Bạn nên làm tuple(sorted(d.items()))để đảm bảo bạn không nhận được các bộ dữ liệu khác nhau cho cùng một cặp khóa-giá trị.
alexis

@alexis Tôi đã làm một vài bài kiểm tra và bạn thực sự đúng. Nếu nhiều khóa được thêm vào giữa và loại bỏ sau đó, thì đó có thể là trường hợp. Cảm ơn rất nhiều về lời bình luận của bạn.
jcollado

Mát mẻ. Tôi đã thêm bản sửa lỗi vào câu trả lời của bạn vì lợi ích của những độc giả tương lai có thể không đọc toàn bộ cuộc hội thoại.
alexis

2
Lưu ý, điều này sẽ không hoạt động nếu bạn tải trong danh sách các ký tự từ jsonmô-đun như tôi đã làm
Dhruv Ghulati

2
Đây là một giải pháp hợp lệ trong trường hợp này, nhưng sẽ không hoạt động trong trường hợp từ điển lồng nhau
Lorenzo Belli

51

Một lớp lót khác dựa trên sự hiểu biết danh sách:

>>> 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}]

Ở đây vì chúng ta có thể sử dụng dictso sánh, chúng tôi chỉ giữ các yếu tố không nằm trong phần còn lại của danh sách ban đầu (khái niệm này chỉ có thể truy cập thông qua chỉ mục n, do đó sử dụng enumerate).


2
Điều này cũng hoạt động cho một danh sách các từ điển bao gồm các danh sách được so sánh với câu trả lời đầu tiên
gbozee

1
điều này cũng hoạt động khi bạn có thể có một loại không thể đánh giá là một giá trị trong từ điển của bạn, không giống như câu trả lời hàng đầu.
Steve Rossiter

1
Ở đây, mục đích là để loại bỏ các giá trị trùng lặp, không phải khóa, hãy xem mã của câu trả lời này
Jamil Noyda

Đây là mã rất không hiệu quả. if i not in d[n + 1:]lặp trên toàn bộ danh sách dicts (từ nnhưng điều đó chỉ nửa tổng số hoạt động) và bạn đang làm mà kiểm tra cho mỗi phần tử trong từ điển của bạn vì vậy đây mã này là O (n ^ 2) thời gian phức tạp
Boris

không hoạt động cho từ điển với từ điển là giá trị
Roko Mijic

22

Các câu trả lời khác sẽ không hoạt động nếu bạn đang hoạt động trên các từ điển lồng nhau, chẳng hạn như các đối tượng JSON được khử lưu huỳnh. Đối với trường hợp này, bạn có thể sử dụng:

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]

1
Tuyệt quá! Thủ thuật là đối tượng dict không thể được thêm trực tiếp vào một tập hợp, nó cần được chuyển đổi thành đối tượng json bằng dump ().
Reihan_amn

18

Nếu sử dụng gói của bên thứ ba sẽ ổn thì bạn có thể sử dụng 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}]

Nó duy trì thứ tự của danh sách gốc và ut cũng có thể xử lý các mục không thể xóa được như từ điển bằng cách quay lại thuật toán chậm hơn ( O(n*m)trong đó ncác phần tử trong danh sách gốc và mcác phần tử duy nhất trong danh sách gốc thay vì O(n)). Trong trường hợp cả khóa và giá trị đều có thể băm, bạn có thể sử dụng keyđối số của hàm đó để tạo các mục có thể băm cho "kiểm tra tính duy nhất" (để nó hoạt động O(n)).

Trong trường hợp từ điển (so sánh độc lập với thứ tự), bạn cần ánh xạ nó tới một cấu trúc dữ liệu khác có thể so sánh như vậy, ví dụ frozenset:

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

Lưu ý rằng bạn không nên sử dụng một tuplecách tiếp cận đơn giản (không sắp xếp) vì các từ điển bằng nhau không nhất thiết phải có cùng một thứ tự (ngay cả trong Python 3.7 trong đó thứ tự chèn - không phải là thứ tự tuyệt đối - được đảm bảo):

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

Và thậm chí sắp xếp bộ dữ liệu có thể không hoạt động nếu các phím không thể sắp xếp:

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

Điểm chuẩn

Tôi nghĩ rằng nó có thể hữu ích để xem hiệu suất của các phương pháp này so sánh như thế nào, vì vậy tôi đã làm một điểm chuẩn nhỏ. Các biểu đồ điểm chuẩn là thời gian so với kích thước danh sách dựa trên danh sách không chứa các mục trùng lặp (được chọn tùy ý, thời gian chạy không thay đổi đáng kể nếu tôi thêm một số hoặc nhiều bản sao). Đó là một biểu đồ log-log để phạm vi hoàn chỉnh được bao phủ.

Thời gian tuyệt đối:

nhập mô tả hình ảnh ở đây

Thời gian liên quan đến cách tiếp cận nhanh nhất:

nhập mô tả hình ảnh ở đây

Cách tiếp cận thứ hai từ thefourtheye là nhanh nhất ở đây. Cách unique_everseentiếp cận với keychức năng nằm ở vị trí thứ hai, tuy nhiên đó là cách tiếp cận nhanh nhất để duy trì trật tự. Các cách tiếp cận khác từ jcolladothefourtheye gần như nhanh chóng. Cách tiếp cận sử dụng unique_everseenkhông có chìa khóa và các giải pháp từ EmmanuelScorpil rất chậm đối với các danh sách dài hơn và hành xử tồi tệ hơn nhiều O(n*n)thay vì O(n). Cách tiếp cận của stpkjson không phải O(n*n)nhưng nó chậm hơn nhiều so với O(n)cách tiếp cận tương tự .

Mã để tái tạo điểm chuẩ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)

Để hoàn thiện ở đây là thời gian cho một danh sách chỉ chứa các bản sao:

# 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)}

nhập mô tả hình ảnh ở đây

Thời gian không thay đổi đáng kể ngoại trừ unique_everseenkhông có keychức năng, trong trường hợp này là giải pháp nhanh nhất. Tuy nhiên, đó chỉ là trường hợp tốt nhất (không đại diện) cho hàm đó với các giá trị không thể thực hiện được vì thời gian chạy của nó phụ thuộc vào số lượng giá trị duy nhất trong danh sách: O(n*m)trong trường hợp này chỉ là 1 và do đó, nó chạy trong O(n).


Tuyên bố miễn trừ trách nhiệm: Tôi là tác giả của iteration_utilities.


15

Đôi khi các vòng lặp kiểu cũ vẫn hữu ích. Mã này dài hơn một chút so với jcollado, nhưng rất dễ đọc:

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])

Các 0 tại range(0, len(a))là không cần thiết.
Juan Antonio

12

Nếu bạn muốn giữ gìn Đơn hàng, thì bạn có thể làm

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

Nếu thứ tự không quan trọng, thì bạn có thể làm

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

Lưu ý: trong python 3, cách tiếp cận thứ hai của bạn cung cấp dict_valuesđầu ra không tuần tự hóa thay vì danh sách. Bạn phải bỏ toàn bộ điều trong một danh sách một lần nữa. list(frozen.....)
saran3h

12

Nếu bạn đang sử dụng Pandas trong quy trình làm việc của mình, một tùy chọn là cung cấp danh sách từ điển trực tiếp cho nhà pd.DataFramexây dựng. Sau đó sử dụng drop_duplicatesto_dictphương pháp cho kết quả cần thiết.

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}]

3

Không phải là một câu trả lời phổ quát , nhưng nếu danh sách của bạn tình cờ được sắp xếp theo một số phím, như thế này:

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}]

thì giải pháp đơn giản như:

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

Kết quả:

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

Làm việc với các từ điển lồng nhau và (rõ ràng) bảo tồn trật tự.


1

Bạn có thể sử dụng một bộ, nhưng bạn cần phải biến các ký tự thành một loại có thể băm.

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)

Duy nhất bây giờ bằng

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

Để lấy lại thông tin:

[dict(x) for x in unique]

Thứ tự d.iteritems()không được đảm bảo - vì vậy bạn có thể kết thúc bằng 'bản sao' unique.
danodonovan

-1

Đây là một giải pháp một dòng nhanh chóng với sự hiểu biết danh sách được lồng ghép đôi (dựa trên giải pháp của @Emmanuel).

Điều này sử dụng một khóa duy nhất (ví dụ a:) trong mỗi dict làm khóa chính, thay vì kiểm tra xem toàn bộ dict có khớp không

[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:]]]

Đó không phải là những gì OP yêu cầu, nhưng đó là điều đã đưa tôi đến chủ đề này, vì vậy tôi nghĩ rằng tôi đã đăng giải pháp mà tôi đã kết thúc với


-1

Không quá ngắn nhưng dễ đọc:

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)

Bây giờ, danh sách list_of_data_uniqsẽ có dicts độc đáo.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.