Python:检查一个字典是否是另一个较大字典的子集


100

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

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

内容:

我有一个字类,并且每个对象都有类似的属性worddefinitionpart_of_speech,等等。我希望能够在这些单词的主列表上调用filter方法,例如Word.objects.filter(word='jump', part_of_speech='verb-intransitive')。我无法弄清楚如何同时管理这些键和值。但是,对于其他人来说,这可能具有更大的功能。

Answers:


108

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

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

优化留给读者练习。


17
如果dict值是可哈希的,那么使用viewitems()是我能想到的最优化的方法:d1.viewitems() <= d2.viewitems()。Timeit运行显示性能提高了3倍。如果不能散列,则即使使用iteritems()代替也items()可以带来约1.2倍的改进。这是使用Python 2.7完成的。
乍得

34
我不认为优化应该留给读者-我担心人们会在没有意识到它会构建superset.items()副本并为子集中的每个项目进行迭代的情况下实际使用它。
罗伯特·金

4
使用Python 3 items()将返回轻量级视图,而不是副本。无需进一步优化。
肯索(Kentzo)

3
嵌套目录呢?
安德烈亚斯·普洛夫斯

5
这真可笑。我将幽默的主题细化交给读者。
深度元素

95

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

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)

1
对于python3,它变为d1.items() <= d2.items()
radu.ciorba,2017年

注意:如果您的程序可能在Python 2.6(甚至更低版本)上使用,d1.items() <= d2.items()则实际上是比较2个元组列表,没有特定顺序,因此最终结果可能不可靠。因此,我切换到@blubberdiblub的答案。
RayLuo

1
d1.items() <= d2.items()是未定义的行为。它没有在官方文档中进行记录,最重要的是,尚未经过测试:github.com/python/cpython/blob / ... 因此,这取决于实现。
罗德里戈·马丁斯·德奥利维拉

2
@RodrigoMartins 此处记录:“对于类似集合的视图,为抽象基类定义的所有操作collections.abc.Set都可用”
augurar

1
@RodrigoMartins如果您担心将来的维护者,请将操作包装在一个明确命名的函数中或添加代码注释。将代码标准降低到不称职的开发人员的水平是一个糟糕的主意。
augurar

36

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

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

但是在3.2中已弃用它,不知道为什么,也许有替代品。


29
很好奇,在3.2的新增功能中发现了这一点: assertDictContainsSubset()方法已被弃用,因为它以错误的顺序错误地实现了参数。这造成了难以调试的错觉,其中TestCase()。assertDictContainsSubset({'a':1,'b':2},{'a':1})之类的测试将失败。 (由Raymond Hettinger提供。)
Pedru

2
等等,左边是预期的,右边是实际的...那不是应该失败吗?该功能唯一的问题是哪个地方混淆了?
JamesHutchison'2

21

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

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


11
如果两个字典中的任何值都不可哈希,则第一个表达式将不起作用。
Pedro Romano

6
通过删除set(d2),可以稍微缩短第二个示例,因为“ issubset接受任何可迭代”。docs.python.org/2/library/stdtypes.html#set
trojjer

这是错误的:d1={'a':1,'b':2}; d2={'a':2,'b':1}->第二个代码段将返回True...
Francesco Pasa

1
@FrancescoPasa第二个片段明确指出:“如果只需要检查密钥”。{'a', 'b'}实际上是{'a', 'b'};)的子集
DylanYoung

19

为了完整起见,您还可以执行以下操作:

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

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


旁注:提到的其他答案small.viewitems() <= big.viewitems()很有希望,但是有一个警告:如果您的程序也可以在Python 2.6(甚至更低版本)上使用,d1.items() <= d2.items()则实际上是比较2个元组列表,没有特定的顺序,因此最终结果可能是不可靠的。因此,我切换到@blubberdiblub的答案。已投票。
RayLuo

这很酷,但似乎不适用于嵌套字典。
Frederik Baetens

@FrederikBaetens并不意味着。另外,我相信没有普遍接受的方式可以做到这一点,因为您可以采用多种方式进行操作,并且可以对此类词典施加多种可能的结构/限制。以下是一些我想到的问题:您如何确定是否应将更深的词典归类?那么具有dict基类类型的对象呢?如果还没有,但仍然表现dict得怎么样呢?如果small并且big在匹配键处包含不同类型的值仍然仍然像dict一样怎么办?
blubberdiblub19年

这些是有效的要点,但是与普通嵌套字典一起使用的基本功能应该很好。我在这里发布了一个示例,但@NutCracker的解决方案更好
Frederik Baetens

当然,如果这是一个关于嵌套字典的问题(并概述了字典的确切要求),我可能会对此有所了解。关键是,当您想知道某个字典是否是另一种从属对象的扁平形式时,嵌套字典的解决方案就无法给出正确的答案(即,当您希望答案严格是False传递的字典的值时匹配键是不同的)。换句话说:嵌套dict的解决方案不一定是用例的替代品。
blubberdiblub19年

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

上下文:

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

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

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)即使d2中包含其他内容,也应返回True,而且它也适用于较低级别:

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子句,并适用于更广泛的情况(例如哈希列表等)。递归也不受限制,因此后果自负。;)


4

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

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

这个看似简单的问题使我花费了几个小时的研究时间才能找到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()Python3。但是有一个警告:它可能有bug。如果您的程序可以在<= 2.6的Python上使用,则它d1.items() <= d2.items()实际上是在比较2个元组列表,没有特定的顺序,因此最终结果将不可靠,并且将成为程序中的一个讨厌的bug。我不希望为Python <= 2.6编写另一种实现,但是我仍然不满意我的代码带有一个已知的错误(即使它在不受支持的平台上)。所以我放弃了这种方法。

  3. 我用@blubberdiblub的答案安定下来(信誉归他所有):

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

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

    • “只有并且当它们具有相同的(键,值)对时,字典的比较才相等。” 是本页的最后一句话
    • “映射(dict的实例)在且仅当它们具有相等的(键,值)对时比较相等。键和元素的相等比较会增强自反性。” 在此页面

2

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

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

注:原来的代码将无法在某些情况下,学分固定@奥利维尔- melançon


代码失败,具有嵌套列表内的字典的超集,在该行if not set(value) <= set(superset[key])
Eelco Hoogendoorn

2

如果您不介意使用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

1

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

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

此函数适用于非哈希值。我也认为它清晰易读。

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

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

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字典。如果有人知道避免这种情况的好方法,而又不像其他答案那样采用部分迭代的解决方案,请告诉我。我需要一种基于键将字典拆分为头部和尾部的方法。

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


1
代码中缺少一些内容。它只是沿着找到的第一个值a(及其后的第一个值)下降popitem。它还应检查同一级别的其他项目。我有成对的嵌套字典,在其中它返回错误的答案。(这里很难提供面向未来的示例,因为它依赖于的顺序popitem
blubberdiblub19年

谢谢,应该立即解决:)
Frederik Baetens
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.