Python: Kiểm tra xem một từ điển có phải là một tập hợp con của một từ điển lớn hơn khác không


100

Tôi đang cố gắng viết một phương pháp lọc tùy chỉnh lấy một số lượng kwargs tùy ý và trả về một danh sách chứa các phần tử của danh sách giống cơ sở dữ liệu có chứa các kwargs đó .

Ví dụ, giả sử d1 = {'a':'2', 'b':'3'}d2= giống nhau. d1 == d2kết quả là True. Nhưng giả sử d2= cùng một thứ cộng với một loạt thứ khác. Phương thức của tôi cần phải biết liệu d1 có trong d2 hay không , nhưng Python không thể làm điều đó với từ điển.

Bối cảnh:

Tôi có một lớp Word, và mỗi đối tượng có tính chất giống như word, definition, part_of_speech, và vân vân. Tôi muốn có thể gọi một phương thức lọc trên danh sách chính của những từ này, như Word.objects.filter(word='jump', part_of_speech='verb-intransitive'). Tôi không thể tìm ra cách quản lý các khóa và giá trị này cùng một lúc. Nhưng điều này có thể có chức năng lớn hơn bên ngoài ngữ cảnh này cho những người khác.

Câu trả lời:


108

Chuyển đổi thành các cặp mục và kiểm tra sự ngăn chặn.

all(item in superset.items() for item in subset.items())

Tối ưu hóa còn lại như một bài tập cho người đọc.


18
Nếu các giá trị dict là hashable, sử dụng viewitems () là cách optimizied nhất mà tôi có thể nghĩ đến: d1.viewitems() <= d2.viewitems(). Thời gian chạy cho thấy sự cải thiện hiệu suất gấp 3 lần. Nếu không thể băm, thậm chí sử dụng iteritems()thay thế cũng items()dẫn đến cải thiện khoảng 1,2 lần. Điều này đã được thực hiện bằng Python 2.7.
Chad

34
Tôi không nghĩ rằng tối ưu hóa nên được để cho người đọc - tôi lo lắng mọi người sẽ thực sự sử dụng điều này mà không nhận ra rằng nó sẽ tạo một bản sao của superset.items () và lặp lại nó cho mọi mục trong tập hợp con.
robert king

4
Với Python 3 items()sẽ trả về các khung nhìn nhẹ thay vì các bản sao. Không cần tối ưu hóa thêm.
Kentzo

3
Điều gì về các thư mục lồng nhau?
Andreas Profous

5
điều này thật vui nhộn. Tôi sẽ để lại chủ đề tinh chỉnh về sự hài hước cho người đọc.
deepelement

95

Trong Python 3, bạn có thể sử dụng dict.items()để có được dạng xem giống như tập hợp của các mục dict. Sau đó, bạn có thể sử dụng <=toán tử để kiểm tra xem một chế độ xem có phải là "tập hợp con" của chế độ xem kia:

d1.items() <= d2.items()

Trong Python 2.7, sử dụng dict.viewitems()để làm tương tự:

d1.viewitems() <= d2.viewitems()

Trong Python 2.6 trở xuống, bạn sẽ cần một giải pháp khác, chẳng hạn như sử dụng all():

all(key in d2 and d2[key] == d1[key] for key in d1)

1
cho python3 này trở thànhd1.items() <= d2.items()
radu.ciorba

Lưu ý: nếu chương trình của bạn có thể được sử dụng trên Python 2.6 (hoặc thậm chí bên dưới), thì d1.items() <= d2.items()chúng thực sự đang so sánh 2 danh sách các bộ giá trị, không có thứ tự cụ thể, vì vậy kết quả cuối cùng có thể sẽ không đáng tin cậy. Vì lý do này, tôi chuyển sang câu trả lời của @blubberdiblub.
RayLuo 17/02/17

1
d1.items() <= d2.items()là hành vi không xác định. Nó không được ghi lại trong tài liệu chính thức và quan trọng nhất, điều này không được kiểm tra: github.com/python/cpython/blob/… Vì vậy, điều này phụ thuộc vào việc triển khai.
Rodrigo Martins de Oliveira

2
@RodrigoMartins Nó ghi lại ở đây : "Đối với thiết như quan điểm, tất cả các hoạt động được định nghĩa cho lớp cơ sở trừu tượng collections.abc.Setcó sẵn"
augurar

1
@RodrigoMartins Nếu bạn lo lắng về những người bảo trì trong tương lai, hãy gói thao tác trong một hàm có tên rõ ràng hoặc thêm một bình luận mã. Hạ thấp các tiêu chuẩn mã của bạn xuống mức của các nhà phát triển không đủ năng lực là một ý tưởng khủng khiếp.
augurar

36

Lưu ý cho những người cần điều này để kiểm tra đơn vị: cũng có một assertDictContainsSubset()phương thức trong TestCaselớp của Python .

http://docs.python.org/2/library/unittest.html?highlight=assertdictcontainssubset#unittest.TestCase.assertDictContainsSubset

Tuy nhiên, nó không được dùng nữa trong 3.2, không rõ tại sao, có thể có một sự thay thế cho nó.


29
rất tò mò, đã tìm thấy điều này trong tính năng mới trong 3.2 : Phương thức khẳng địnhDictContainsSubset () không được dùng nữa vì nó được thực hiện sai với các đối số không đúng thứ tự. Điều này đã tạo ra ảo ảnh quang học khó gỡ lỗi trong đó các thử nghiệm như TestCase (). Khẳng địnhDictContainsSubset ({'a': 1, 'b': 2}, {'a': 1}) sẽ không thành công. (Đóng góp bởi Raymond Hettinger.)
Pedru

2
Chờ đã, phía bên trái được mong đợi và phía bên phải là thực tế ... Điều đó sẽ không thất bại chứ? Điều duy nhất sai với chức năng là cái nào đi vào chỗ nào là khó hiểu?
JamesHutchison

21

để kiểm tra khóa và giá trị sử dụng: set(d1.items()).issubset(set(d2.items()))

nếu bạn chỉ cần kiểm tra các khóa: set(d1).issubset(set(d2))


11
Biểu thức đầu tiên sẽ không hoạt động nếu không thể băm bất kỳ giá trị nào trong một trong hai từ điển.
Pedro Romano

6
Ví dụ thứ hai có thể được rút ngắn một chút bằng cách loại bỏ tập hợp (d2), vì "Issubset chấp nhận mọi thứ có thể lặp lại". docs.python.org/2/library/stdtypes.html#set
trojjer

Điều này sai: d1={'a':1,'b':2}; d2={'a':2,'b':1}-> đoạn mã thứ hai sẽ trả về True...
Francesco Pasa

1
@FrancecoPasa Đoạn mã thứ hai nói rõ ràng: "nếu bạn chỉ cần kiểm tra các khóa". {'a', 'b'}trên thực tế là một tập hợp con của {'a', 'b'};)
DylanYoung

19

Để hoàn thiện, bạn cũng có thể làm điều này:

def is_subdict(small, big):
    return dict(big, **small) == big

Tuy nhiên, tôi không đưa ra bất kỳ tuyên bố nào liên quan đến tốc độ (hoặc thiếu) hoặc khả năng đọc (hoặc thiếu).


Một lưu ý phụ: Các câu trả lời khác small.viewitems() <= big.viewitems()được đề cập là đầy hứa hẹn, nhưng có một lưu ý: nếu chương trình của bạn cũng có thể được sử dụng trên Python 2.6 (hoặc thậm chí bên dưới), thì d1.items() <= d2.items()chúng thực sự đang so sánh 2 danh sách các bộ dữ liệu, không có thứ tự cụ thể, vì vậy kết quả cuối cùng có thể sẽ không đáng tin cậy. Vì lý do đó, tôi chuyển sang câu trả lời của @blubberdiblub. Đã ủng hộ.
RayLuo 17/02/17

Điều này thật tuyệt, nhưng dường như nó không hoạt động với các từ điển lồng nhau.
Frederik Baetens

@FrederikBaetens không phải vậy. Ngoài ra, tôi tin rằng không có cách nào được chấp nhận chung để làm điều đó, bởi vì có nhiều cách bạn có thể sử dụng và có nhiều cấu trúc / hạn chế có thể có mà bạn có thể áp đặt cho các từ điển như vậy. Dưới đây là một số câu hỏi xuất hiện trong đầu bạn: Làm thế nào để bạn xác định xem có nên sử dụng một từ điển sâu hơn hay không? Điều gì về các đối tượng của một kiểu có dictlớp cơ sở? Điều gì sẽ xảy ra nếu nó không có và vẫn hoạt động như một dict? Điều gì sẽ xảy ra nếu smallbigchứa các giá trị thuộc loại khác nhau tại một khóa phù hợp vẫn hoạt động giống như dict?
blubberdiblub

Đó là những điểm hợp lệ, nhưng một chức năng cơ bản hoạt động với các phần lồng nhau đơn giản sẽ rất hay. Tôi đã đăng một ví dụ ở đây , nhưng giải pháp của @ NutCracker tốt hơn
Frederik Baetens

Chắc chắn, nếu đó là một câu hỏi về các từ điển lồng nhau (và yêu cầu chính xác cho các từ điển được nêu ra), tôi có thể đã có một vết nứt ở nó. Vấn đề là một giải pháp cho các từ điển lồng nhau không đưa ra câu trả lời đúng khi bạn muốn biết liệu một mệnh lệnh có phải là một mệnh lệnh con của một mệnh đề khác theo cách không (tức là khi bạn muốn câu trả lời chính xác là Falsekhi giá trị của các ô được truyền khác nhau đối với các khóa khớp). Hay nói cách khác: Giải pháp cho các phần tử lồng nhau không nhất thiết phải là sự thay thế thả xuống tùy thuộc vào trường hợp sử dụng.
blubberdiblub

10
>>> d1 = {'a':'2', 'b':'3'}
>>> d2 = {'a':'2', 'b':'3','c':'4'}
>>> all((k in d2 and d2[k]==v) for k,v in d1.iteritems())
True

bối cảnh:

>>> d1 = {'a':'2', 'b':'3'}
>>> d2 = {'a':'2', 'b':'3','c':'4'}
>>> list(d1.iteritems())
[('a', '2'), ('b', '3')]
>>> [(k,v) for k,v in d1.iteritems()]
[('a', '2'), ('b', '3')]
>>> k,v = ('a','2')
>>> k
'a'
>>> v
'2'
>>> k in d2
True
>>> d2[k]
'2'
>>> k in d2 and d2[k]==v
True
>>> [(k in d2 and d2[k]==v) for k,v in d1.iteritems()]
[True, True]
>>> ((k in d2 and d2[k]==v) for k,v in d1.iteritems())
<generator object <genexpr> at 0x02A9D2B0>
>>> ((k in d2 and d2[k]==v) for k,v in d1.iteritems()).next()
True
>>> all((k in d2 and d2[k]==v) for k,v in d1.iteritems())
True
>>>

4

Hàm của tôi cho cùng mục đích, thực hiện điều này một cách đệ quy:

def dictMatch(patn, real):
    """does real dict match pattern?"""
    try:
        for pkey, pvalue in patn.iteritems():
            if type(pvalue) is dict:
                result = dictMatch(pvalue, real[pkey])
                assert result
            else:
                assert real[pkey] == pvalue
                result = True
    except (AssertionError, KeyError):
        result = False
    return result

Trong ví dụ của bạn, dictMatch(d1, d2)nên trả về True ngay cả khi d2 có nội dung khác trong đó, cộng với nó cũng áp dụng cho các cấp thấp hơn:

d1 = {'a':'2', 'b':{3: 'iii'}}
d2 = {'a':'2', 'b':{3: 'iii', 4: 'iv'},'c':'4'}

dictMatch(d1, d2)   # True

Lưu ý: Có thể còn có giải pháp tốt hơn để tránh if type(pvalue) is dictmệnh đề và áp dụng cho nhiều trường hợp hơn nữa (như danh sách các hàm băm, v.v.). Ngoài ra đệ quy không bị giới hạn ở đây vì vậy hãy tự chịu rủi ro khi sử dụng. ;)


4

Đây là một giải pháp cũng có thể đệ quy đúng cách thành các danh sách và tập hợp có trong từ điển. Bạn cũng có thể sử dụng điều này cho các danh sách có chứa số, v.v.

def is_subset(subset, superset):
    if isinstance(subset, dict):
        return all(key in superset and is_subset(val, superset[key]) for key, val in subset.items())

    if isinstance(subset, list) or isinstance(subset, set):
        return all(any(is_subset(subitem, superitem) for superitem in superset) for subitem in subset)

    # assume that subset is a plain value if none of the above match
    return subset == superset

2

Vấn đề có vẻ đơn giản này khiến tôi mất vài giờ nghiên cứu để tìm ra giải pháp đáng tin cậy 100%, vì vậy tôi đã ghi lại những gì tôi tìm thấy trong câu trả lời này.

  1. Nói "Pythonic-ally", small_dict <= big_dictsẽ là cách trực quan nhất, nhưng quá tệ là nó sẽ không hoạt động . {'a': 1} < {'a': 1, 'b': 2}dường như hoạt động trong Python 2, nhưng nó không đáng tin cậy vì tài liệu chính thức gọi nó một cách rõ ràng. Tìm kiếm "Các kết quả khác với bình đẳng được giải quyết nhất quán, nhưng không được định nghĩa khác." trong phần này . Chưa kể, việc so sánh 2 phái trong Python 3 dẫn đến ngoại lệ TypeError.

  2. Điều trực quan thứ hai small.viewitems() <= big.viewitems()chỉ dành cho Python 2.7 và small.items() <= big.items()Python 3. Nhưng có một cảnh báo: nó có khả năng bị lỗi . Nếu chương trình của bạn có khả năng được sử dụng trên Python <= 2.6, chương trình của bạn d1.items() <= d2.items()thực sự đang so sánh 2 danh sách các bộ dữ liệu, không có thứ tự cụ thể, vì vậy kết quả cuối cùng sẽ không đáng tin cậy và nó trở thành một lỗi khó chịu trong chương trình của bạn. Tôi không muốn viết thêm một triển khai khác cho Python <= 2.6, nhưng tôi vẫn không cảm thấy thoải mái khi mã của mình có một lỗi đã biết (ngay cả khi nó nằm trên nền tảng không được hỗ trợ). Vì vậy, tôi từ bỏ cách tiếp cận này.

  3. Tôi ổn định với câu trả lời của @blubberdiblub (Tín dụng thuộc về anh ấy):

    def is_subdict(small, big): return dict(big, **small) == big

    Cần chỉ ra rằng, câu trả lời này dựa trên ==hành vi giữa các phần, được xác định rõ ràng trong tài liệu chính thức, do đó sẽ hoạt động trong mọi phiên bản Python . Đi tìm kiếm:

    • "Các từ điển so sánh bằng nhau nếu và chỉ khi chúng có các cặp (khóa, giá trị) giống nhau." là câu cuối cùng trong trang này
    • "Các ánh xạ (trường hợp của dict) so sánh bằng nhau nếu và chỉ khi chúng có các cặp (khóa, giá trị) bằng nhau. So sánh ngang bằng giữa các khóa và phần tử thực thi tính phản xạ." trong trang này

2

Đây là một giải pháp đệ quy chung cho vấn đề đã cho:

import traceback
import unittest

def is_subset(superset, subset):
    for key, value in subset.items():
        if key not in superset:
            return False

        if isinstance(value, dict):
            if not is_subset(superset[key], value):
                return False

        elif isinstance(value, str):
            if value not in superset[key]:
                return False

        elif isinstance(value, list):
            if not set(value) <= set(superset[key]):
                return False
        elif isinstance(value, set):
            if not value <= superset[key]:
                return False

        else:
            if not value == superset[key]:
                return False

    return True


class Foo(unittest.TestCase):

    def setUp(self):
        self.dct = {
            'a': 'hello world',
            'b': 12345,
            'c': 1.2345,
            'd': [1, 2, 3, 4, 5],
            'e': {1, 2, 3, 4, 5},
            'f': {
                'a': 'hello world',
                'b': 12345,
                'c': 1.2345,
                'd': [1, 2, 3, 4, 5],
                'e': {1, 2, 3, 4, 5},
                'g': False,
                'h': None
            },
            'g': False,
            'h': None,
            'question': 'mcve',
            'metadata': {}
        }

    def tearDown(self):
        pass

    def check_true(self, superset, subset):
        return self.assertEqual(is_subset(superset, subset), True)

    def check_false(self, superset, subset):
        return self.assertEqual(is_subset(superset, subset), False)

    def test_simple_cases(self):
        self.check_true(self.dct, {'a': 'hello world'})
        self.check_true(self.dct, {'b': 12345})
        self.check_true(self.dct, {'c': 1.2345})
        self.check_true(self.dct, {'d': [1, 2, 3, 4, 5]})
        self.check_true(self.dct, {'e': {1, 2, 3, 4, 5}})
        self.check_true(self.dct, {'f': {
            'a': 'hello world',
            'b': 12345,
            'c': 1.2345,
            'd': [1, 2, 3, 4, 5],
            'e': {1, 2, 3, 4, 5},
        }})
        self.check_true(self.dct, {'g': False})
        self.check_true(self.dct, {'h': None})

    def test_tricky_cases(self):
        self.check_true(self.dct, {'a': 'hello'})
        self.check_true(self.dct, {'d': [1, 2, 3]})
        self.check_true(self.dct, {'e': {3, 4}})
        self.check_true(self.dct, {'f': {
            'a': 'hello world',
            'h': None
        }})
        self.check_false(
            self.dct, {'question': 'mcve', 'metadata': {'author': 'BPL'}})
        self.check_true(
            self.dct, {'question': 'mcve', 'metadata': {}})
        self.check_false(
            self.dct, {'question1': 'mcve', 'metadata': {}})

if __name__ == "__main__":
    unittest.main()

LƯU Ý: Mã gốc sẽ không thành công trong một số trường hợp nhất định, tín dụng cho việc sửa chữa sẽ được chuyển đến @ olivier-melançon


code cũng sẽ không có một superset mà có một dict lồng vào bên trong một danh sách, trong dòngif not set(value) <= set(superset[key])
Eelco Hoogendoorn

2

Nếu bạn không phiền khi sử dụng pydash thì is_matchcó nó sẽ thực hiện chính xác điều đó:

import pydash

a = {1:2, 3:4, 5:{6:7}}
b = {3:4.0, 5:{6:8}}
c = {3:4.0, 5:{6:7}}

pydash.predicates.is_match(a, b) # False
pydash.predicates.is_match(a, c) # True

1

Tôi biết câu hỏi này đã cũ, nhưng đây là giải pháp của tôi để kiểm tra xem một từ điển lồng nhau có phải là một phần của từ điển lồng nhau khác hay không. Giải pháp là đệ quy.

def compare_dicts(a, b):
    for key, value in a.items():
        if key in b:
            if isinstance(a[key], dict):
                if not compare_dicts(a[key], b[key]):
                    return False
            elif value != b[key]:
                return False
        else:
            return False
    return True

0

Hàm này hoạt động đối với các giá trị không thể băm. Tôi cũng nghĩ rằng nó rõ ràng và dễ đọc.

def isSubDict(subDict,dictionary):
    for key in subDict.keys():
        if (not key in dictionary) or (not subDict[key] == dictionary[key]):
            return False
    return True

In [126]: isSubDict({1:2},{3:4})
Out[126]: False

In [127]: isSubDict({1:2},{1:2,3:4})
Out[127]: True

In [128]: isSubDict({1:{2:3}},{1:{2:3},3:4})
Out[128]: True

In [129]: isSubDict({1:{2:3}},{1:{2:4},3:4})
Out[129]: False

0

Một triển khai đệ quy ngắn hoạt động cho các từ điển lồng nhau:

def compare_dicts(a,b):
    if not a: return True
    if isinstance(a, dict):
        key, val = a.popitem()
        return isinstance(b, dict) and key in b and compare_dicts(val, b.pop(key)) and compare_dicts(a, b)
    return a == b

Điều này sẽ tiêu thụ các đường a và b. Nếu ai biết cách hay để tránh điều đó mà không cần dùng đến các giải pháp lặp lại một phần như trong các câu trả lời khác, vui lòng cho tôi biết. Tôi sẽ cần một cách để tách một câu lệnh thành phần đầu và phần đuôi dựa trên một khóa.

Mã này hữu ích hơn như một bài tập lập trình và có lẽ chậm hơn rất nhiều so với các giải pháp khác ở đây kết hợp đệ quy và lặp lại. Giải pháp của @ Nutcracker khá tốt cho các từ điển lồng nhau.


1
Có điều gì đó bị thiếu trong mã. Nó chỉ giảm xuống giá trị đầu tiên bắt đầu trong a(và bất kỳ giá trị đầu tiên nào tiếp theo) được popitemtìm thấy. Nó cũng nên kiểm tra các mục khác trên cùng cấp độ. Tôi đã có các cặp di lệnh lồng nhau mà nó trả về câu trả lời sai. (khó có thể đưa ra một ví dụ tương lai chứng minh ở đây, vì nó phụ thuộc vào thứ tự của popitem)
blubberdiblub

Cảm ơn, sẽ được sửa ngay bây giờ :)
Frederik Baetens

0

Sử dụng đối tượng trình bao bọc này để cung cấp so sánh từng phần và khác biệt tốt:


class DictMatch(dict):
    """ Partial match of a dictionary to another one """
    def __eq__(self, other: dict):
        assert isinstance(other, dict)
        return all(other[name] == value for name, value in self.items())

actual_name = {'praenomen': 'Gaius', 'nomen': 'Julius', 'cognomen': 'Caesar'}
expected_name = DictMatch({'praenomen': 'Gaius'})  # partial match
assert expected_name == actual_name  # True
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.