繁体   English   中英

如何检查一本字典是否是另一本更大字典的子集?

[英]How to check if one dictionary is a subset of another larger dictionary?

我正在尝试编写一个自定义过滤器方法,该方法接受任意数量的kwargs并返回一个列表,该列表包含包含这些kwargs的类似数据库的列表的元素。

例如,假设d1 = {'a':'2', 'b':'3'}d2 = 相同的东西。 d1 == d2结果为 True。 但是假设d2 = 同样的东西加上一堆其他的东西。 我的方法需要能够判断d2 中的 d1 是否存在,但 Python 不能用字典做到这一点。

语境:

我有一个 Word 类,每个对象都有像worddefinitionpart_of_speech等属性。 我希望能够在这些单词的主列表Word.objects.filter(word='jump', part_of_speech='verb-intransitive')滤器方法,例如Word.objects.filter(word='jump', part_of_speech='verb-intransitive') 我不知道如何同时管理这些键和值。 但是对于其他人来说,这可能在此上下文之外具有更大的功能。

在 Python 3 中,您可以使用dict.items()来获取 dict 项目的类似集合的视图。 然后,您可以使用<=运算符来测试一个视图是否是另一个视图的“子集”:

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

在 Python 2.7 中,使用dict.viewitems()来做同样的事情:

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

在 Python 2.6 及以下版本中,您将需要不同的解决方案,例如使用all()

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

转换为项目对并检查是否包含。

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

优化留给读者作为练习。

请注意需要它进行单元测试的人:Python 的TestCase类中还有一个assertDictContainsSubset()方法。

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

然而它在 3.2 中被弃用了,不知道为什么,也许有一个替代品。

对于键和值检查使用: set(d1.items()).issubset(set(d2.items()))

如果您只需要检查键: set(d1).issubset(set(d2))

为了完整起见,您还可以这样做:

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

但是,我对速度(或缺乏速度)或可读性(或缺乏速度)不做任何声明。

更新:正如鲍里斯的评论所指出的那样,如果您的小字典具有非字符串键并且您使用的是 Python >= 3(或者换句话说:面对任意键入的键,它只能工作在旧版 Python 2.x 中)。

但是,如果您使用的是Python 3.9 或更高版本,则可以使其与非字符串类型的键一起使用,并获得更简洁的语法。

如果您的代码已经将两个字典作为变量,那么检查这个内联会非常简洁:

if big | small == big:
    # do something

否则,或者如果您更喜欢上述可重用的功能,则可以使用:

def is_subdict(small, big):
    return big | small == big

工作原理与第一个函数相同,只是这次使用了扩展以支持字典的联合运算符。

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

语境:

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

这是一个解决方案,它也可以正确地递归到字典中包含的列表和集合中。 您也可以将它用于包含字典等的列表...

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

我的函数出于同样的目的,递归地执行此操作:

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

在您的示例中, dictMatch(d1, d2)应该返回 True 即使 d2 中包含其他内容,而且它也适用于较低级别:

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

dictMatch(d1, d2)   # True

注意:可能有更好的解决方案可以避免if type(pvalue) is dict子句并适用于更广泛的情况(如哈希列表等)。 此外,递归在这里不受限制,因此使用风险自负。 ;)

这个看似简单的问题花费了我几个小时的研究时间来找到一个 100% 可靠的解决方案,所以我记录了我在这个答案中发现的内容。

  1. “Pythonic-ally”说, small_dict <= big_dict将是最直观的方式,但太糟糕了,它不起作用 {'a': 1} < {'a': 1, 'b': 2}看似在 Python 2 中有效,但它并不可靠,因为官方文档明确指出它。 去搜索“平等以外的结果得到一致解决,但没有另外定义。” 本节中 更不用说,比较 Python 3 中的 2 个字典会导致 TypeError 异常。

  2. 第二个最直观的事情是small.viewitems() <= big.viewitems()仅适用于 Python 2.7,而small.items() <= big.items()适用于 Python 3。但有一个警告:它可能是马车 如果你的程序有可能在 Python <=2.6 上使用,它的d1.items() <= d2.items()实际上是在比较 2 个元组列表,没有特定的顺序,所以最终结果将是不可靠的,它变得令人讨厌程序中的错误。 我并不热衷于为 Python<=2.6 编写另一个实现,但我仍然觉得我的代码带有已知错误(即使它位于不受支持的平台上)。 所以我放弃了这个方法。

  3. 我对@blubberdiblub 的回答安定下来(归功于他):

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

    值得指出的是,这个答案依赖于 dicts 之间的==行为,这在官方文档中有明确定义,因此应该适用于每个 Python 版本 去搜索:

    • “字典比较相等当且仅当它们具有相同的(键,值)对。” 这个页面的最后一句话
    • “映射(字典的实例)当且仅当它们具有相等的(键,值)对时才比较相等。键和元素的相等比较强制自反性。” 这个页面

这是给定问题的一般递归解决方案:

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

注意:原始代码在某些情况下会失败,修复 的功劳归于 @olivier-melançon

如果您不介意使用pydash那里有is_match可以做到这一点:

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

我知道这个问题很老,但这是我检查一个嵌套字典是否是另一个嵌套字典的一部分的解决方案。 解决方案是递归的。

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

使用这个提供部分比较和良好差异的包装器对象:


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

如果在 dict 中有一些其他 dicts 的数组,大多数答案将不起作用,这是一个解决方案:

def d_eq(d, d1):
   if not isinstance(d, (dict, list)):
      return d == d1
   if isinstance(d, list):
      return all(d_eq(a, b) for a, b in zip(d, d1))
   return all(d.get(i) == d1[i] or d_eq(d.get(i), d1[i]) for i in d1)

def is_sub(d, d1):
  if isinstance(d, list):
     return any(is_sub(i, d1) for i in d)
  return d_eq(d, d1) or (isinstance(d, dict) and any(is_sub(b, d1) for b in d.values()))

print(is_sub(dct_1, dict_2))

取自如何检查 dict 是否是另一个复杂 dict 的子集

这样做的另一种方法:

>>> d1 = {'a':'2', 'b':'3'}
>>> d2 = {'a':'2', 'b':'3','c':'4'}
>>> d3 = {'a':'1'}
>>> set(d1.items()).issubset(d2.items())
True
>>> set(d3.items()).issubset(d2.items())
False

使用 Python 3.9,这就是我使用的:

def dict_contains_dict(small: dict, big: dict):    
   return (big | small) == big

此函数适用于不可散列的值。 我也认为它清晰易读。

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

适用于嵌套字典的简短递归实现:

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

这将消耗 a 和 b 字典。 如果有人知道避免这种情况的好方法,而无需像其他答案那样求助于部分迭代解决方案,请告诉我。 我需要一种基于键将 dict 拆分为头部和尾部的方法。

此代码作为编程练习更有用,并且可能比此处混合递归和迭代的其他解决方案慢得多。 @Nutcracker 的解决方案非常适合嵌套字典。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM