如何检查一个字典是否是另一个较大的字典的子集?

167

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

例如,假设d1 = {'a':'2', 'b':'3'}d2是相同的。 d1 == d2 结果为True。但是如果d2是相同的加上一堆其他东西。我的方法需要能够告诉是否d1在d2中,但Python无法用字典来完成。

背景:

我有一个Word类,每个对象都有像worddefinitionpart_of_speech等属性。我想能够在这些单词的主列表上调用一个过滤方法,如Word.objects.filter(word='jump', part_of_speech='verb-intransitive')。但我无法同时管理这些键和值。但是,这对其他人可能具有更大的功能性外部上下文。


3
使用Python 3.9+中的字典合并运算符,将subset_dict合并到my_dict中:my_dict == my_dict | subset_dict - user3064538
19个回答

214
在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
注意:如果您的程序可能在Python 2.6(甚至更低版本)上使用,则 d1.items() <= d2.items() 实际上是比较两个元组列表,没有特定的顺序,因此最终结果可能不可靠。出于这个原因,我转而使用 @blubberdiblub 的答案。 - RayLuo
@RayLuo 是的,viewitems和items不同。编辑以澄清。 - augurar
2
d1.items() <= d2.items() 是未定义的行为。它在官方文档中没有记录,最重要的是,这也没有经过测试:https://github.com/python/cpython/blob/2e576f5aec1f8f23f07001e2eb3db9276851a4fc/Lib/test/test_dictviews.py#L146因此,这取决于实现。 - Rodrigo Oliveira
5
在这里有相关记录:[https://docs.python.org/3/library/stdtypes.html#dict-views]。对于类似集合的视图,所有在抽象基类[collections.abc.Set](https://docs.python.org/3/library/collections.abc.html#collections.abc.Set)中定义的操作都可用。 - augurar
@augurar 你说得对,我错过了这个。虽然我仍然不喜欢这种解决方案会对不知道这种行为的任何开发人员造成多大的困惑。我会采用 blubberdiblub 建议的更明确的实用函数 (https://dev59.com/c2ox5IYBdhLWcg3wQSIO#35416899)。 - Rodrigo Oliveira
4
如果你担心未来的维护者,可以将操作封装在一个清晰命名的函数中或添加代码注释。但是,为了迎合无能力的开发人员而降低代码标准是不明智的做法。 - augurar

145

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

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

优化留给读者自行实践。


21
如果字典的值是可哈希的,我认为使用viewitems()是最优化的方式:d1.viewitems() <= d2.viewitems()。时间测试表明性能提升了3倍以上。如果不可哈希,即使使用iteritems()代替items()也会有1.2倍的提升。这是在Python 2.7中进行的。 - Chad
51
我认为优化不应该留给读者自己去做——我担心人们会在不意识到它会复制 superset.items() 并针对子集中的每个项目进行迭代的情况下使用它。 - Rusty Rob
10
使用Python 3时,items()方法将返回轻量级视图而不是副本。无需进行进一步的优化。 - Kentzo
4
嵌套目录怎么办? - Andreas Profous
16
这真是太搞笑了。我会把幽默的细节留给读者来体会。 - deepelement
显示剩余2条评论

44

34
我看到这篇Python 3.2新特性,发现以下内容:由于参数顺序错误,assertDictContainsSubset()方法已被弃用。这会导致一些难以调试的视觉错觉,例如TestCase().assertDictContainsSubset({'a':1, 'b':2}, {'a':1})测试将失败。(由Raymond Hettinger贡献) - Pedru
2
等等,左边应该是期望值,右边是实际值……这不应该失败吗?函数唯一的问题就是哪个参数放在哪个位置有点混淆? - JamesHutchison
@JamesHutchison 正确 - user3064538

38

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

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

然而,我对速度(或其缺乏)和可读性(或其缺乏)不作任何声明。

更新:正如Boris的评论所指出的,如果您的小字典具有非字符串键并且您正在使用Python >= 3(或者换句话说:在任意类型的键面前,它仅在旧版Python 2.x中起作用),则此技巧将不起作用。

但是,如果您使用的是Python 3.9或更高版本,则可以使其同时适用于非字符串类型的键,以及获得更简洁的语法。

假设您的代码已经有两个字典变量,那么检查它是否行内非常简洁:

if big | small == big:
    # do something
否则,或者如果您喜欢像上面那样使用可重用函数,您可以使用以下内容:
def is_subdict(small, big):
    return big | small == big

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


一个附注:其他回答提到small.viewitems() <= big.viewitems()看起来是有希望的,但是有一个条件:如果你的程序可能也会在Python 2.6(甚至更低)上使用,那么d1.items() <= d2.items()实际上比较的是两个元组列表,没有特定的顺序,所以最终的结果可能不可靠。因此,我选择了@blubberdiblub的答案。点赞。 - RayLuo
1
这很酷,但似乎无法处理嵌套字典。 - Frederik Baetens
这些都是有道理的观点,但一个能够与普通嵌套字典一起工作的基本函数会很好。我在这里发布了一个示例,但@NutCracker的解决方案更好。 - Frederik Baetens
当然,如果这是一个嵌套字典的问题(并且确切的字典要求已经概述),我可能会尝试解决它。关键在于嵌套字典的解决方案在扁平化方式下无法给出正确答案(例如当您希望传递的字典值匹配键时,严格的回答只能为“False”)。换句话说:嵌套字典的解决方案根据使用情况不一定是可插入式替代方案。 - blubberdiblub
3
"{**big, **small} == big" 这个语句也可以用在之前的 Python 版本中,不必等到 3.9 版本。 - ddejohn
显示剩余7条评论

27

如果需要检查字典 d1 的键和值是否都在字典 d2 中存在,可以使用以下代码:

set(d1.items()).issubset(set(d2.items()))

如果只需要检查字典 d1 的键是否都在字典 d2 中存在,可以使用以下代码:

set(d1).issubset(set(d2))

12
如果任一字典中的值不可哈希,第一个表达式将无法运行。 - Pedro Romano
6
第二个例子可以稍微简化一下,删除set(d2),因为“issubset接受任何可迭代对象”。参见http://docs.python.org/2/library/stdtypes.html#set。 - trojjer
这是错误的:d1={'a':1,'b':2}; d2={'a':2,'b':1} -> 第二个代码片段将返回 True... - Francesco Pasa
3
第二段明确表示:“如果你只需要检查键”。{'a','b'}实际上是{'a','b'}的子集;) - DylanYoung

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

9
这里有一个解决方案,它还可以适当地递归进入字典中包含的列表和集合。此方法也可用于包含字典等的列表。
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

在使用Python 3.10时,您可以使用Python的新匹配语句进行类型检查:
def is_subset(subset, superset):
    match subset:
        case dict(_): return all(key in superset and is_subset(val, superset[key]) for key, val in subset.items())
        case list(_) | 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
        case _: return subset == superset

这对于 {"name" : "Girls"} 的子集和 {"show": {"name": "Girls"}} 的超集失败。 - Ywapom
3
这是因为它不是一个子集。如果我们将字典视为可搜索的(键值)元组集合,那么两个集合都有一个元素:一个具有键值元组"name":"Girls",另一个具有由键值元组"show": {"name": "Girls"}组成的单个元素。当两个集合都有一个元素时,这些元素必须相等才能被认为是彼此的子集。显然这不是这种情况,它们都是键值元组,但一个具有键名,另一个具有键show,一个具有值Girls,另一个具有另一个键值元组形式的值。 - Frederik Baetens
即使使用我定义的递归子集,它放宽了“对于子集中的每个元素,超集中应该有一个等于该元素的元素”的要求,变成了“对于子集中的每个元素,超集中应该有一个元素,其为该元素的子集”,你的集合也不是子集,因为元素出现的级别很重要。将家族谱系想象成字典:{grandpa: {child1:{}, child2:{}}} 不应该是 {grandpa: {dad: {child1:{}, child2:{}}}} 的子集。 - Frederik Baetens
只是提醒一下,从技术上讲,如果两个事物是等价的,那么根据集合论,它们都是彼此的子集。但在我自己的通俗语言(也许是许多人的通俗语言)中,等价的事物不是彼此的子集;这对我来说没有意义;它们是等价的。因此,如果你和我一样,你可能也想检查它们是否等价。 - Joe Flack
是的,你可以在我上面写的句子中用"equivalent"代替"equal"。 - Frederik Baetens

7

使用 Python 3.9,这是我使用的方法:

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

请更加详细地说明。原帖的解释不够清楚。反例(根据解释而定):small = {'pl' : 'key1': {'key2': 'value2', 'otherkey'..}},第二个反例 small = {'pl' : 'key1': {'key2': {'value2', 'otherkey'..}}}。 - Jay-Pi
你能详细说明一下你的反例吗?smallbig具体是什么值? - Andreas Profous

3

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

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

1
除了名称之外,这看起来像是最干净的解决方案。非常遗憾的是,Python没有提供针对这类标准事务的libstd方法。我会使用def has_fieldsvals(small: dict, big: dict) -> bool:作为方法签名。 - Jay-Pi
1
除了名字之外,这看起来是最干净的解决方案。很遗憾,Python没有提供libstd方法来处理这种标准的事情。我会使用def has_fieldsvals(small: dict, big: dict) -> bool:作为方法的签名。 - undefined

3

我编写了一个递归函数实现相同的功能:

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语句,并适用于更广泛的情况(如哈希列表等)。此外,递归在此处不受限制,请自行决定是否使用。 ;)

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接