Python:ある辞書が別の大きな辞書のサブセットであるかどうかを確認する


100

任意の数のkwargsを受け取り、それらのkwargsを含むデータベースのようなリストの要素を含むリストを返すカスタムフィルターメソッドを作成しようとしています。

たとえば、仮定d1 = {'a':'2', 'b':'3'}d2=は同じことです。d1 == d2Trueになります。しかし、d2=同じことと他の多くのことを考えてみてください。私のメソッドはd1がd2であるかどうかを判別できる必要がありますが、Pythonは辞書でそれを行うことができません。

環境:

私は、Wordのクラスがあり、各オブジェクトは、同様のプロパティがありworddefinitionpart_of_speech、などを。のように、これらの単語のメインリストでフィルタメソッドを呼び出せるようにしたいWord.objects.filter(word='jump', part_of_speech='verb-intransitive')。これらのキーと値を同時に管理する方法がわかりません。しかし、これは他の人にとってはこのコンテキストの外でより大きな機能を持つことができます。

回答:


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 2017

3
ネストされたディレクトリはどうですか?
Andreas Profous

5
こりゃ愉快だ。ユーモアの主題の洗練は読者に任せます。
deepelement

95

Python 3では、を使用dict.items()して、dict項目のセットのようなビューを取得できます。次に、<=演算子を使用して、1つのビューが他のビューの「サブセット」であるかどうかをテストできます。

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

警告:プログラムがPython 2.6(またはそれ以下)で使用される可能性がある場合、d1.items() <= d2.items()実際には2つのタプルのリストを特定の順序なしで比較しているため、最終結果はおそらく信頼できません。このため、@ blubberdiblubの回答に切り替えます。
RayLuo 2017

1
d1.items() <= d2.items()未定義の動作です。公式ドキュメントには記載されておらず、最も重要なこととして、これはテストされていません:github.com/python/cpython/blob/… したがって、これは実装に依存します。
ロドリゴマルティンスデオリベイラ

2
@RodrigoMartinsそれはここに文書化されています:「セットのようなビューの場合、抽象基本クラスに定義されたすべての操作collections.abc.Setが利用可能です」
augurar

1
@RodrigoMartins将来のメンテナが心配な場合は、操作をわかりやすい名前の関数でラップするか、コードコメントを追加してください。コード標準を無能な開発者のレベルまで下げるのはひどい考えです。
オーグラ2017年

36

単体テストにこれを必要とする人のための注意:assertDictContainsSubset()PythonのTestCaseクラスにはメソッドもあります。

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

ただし、3.2では非推奨になりました。理由は不明ですが、代わりのものがある可能性があります。


29
好奇心、でこれを発見された3.2の新機能それは間違った順序で引数をmisimplementedたのでassertDictContainsSubset()メソッドは廃止されました。これにより、TestCase()。assertDictContainsSubset({'a':1、 'b':2}、{'a':1})などのテストが失敗する、デバッグが困難な錯覚が作成されました。 (Raymond Hettingerによる寄稿)
Pedru

2
待って、左側が期待されており、右側が実際のものです...それは失敗するべきではないですか?関数の唯一の問題は、どちらがどこに行くのかが混乱していることです。
JamesHutchison 2017

21

キーと値については、使用を確認してください: set(d1.items()).issubset(set(d2.items()))

キーのみをチェックする必要がある場合: set(d1).issubset(set(d2))


11
いずれかの辞書の値がハッシュ可能でない場合、最初の式は機能しません。
ペドロロマーノ

6
2番目の例は、set(d2)を削除することで少し短縮できます。「issubsetは任意の反復可能オブジェクトを受け入れる」ためです。docs.python.org/2/library/stdtypes.html#set
trojjer 2013

これは間違っています:d1={'a':1,'b':2}; d2={'a':2,'b':1}-> 2番目のスニペットが返されTrueます...
Francesco Pasa

1
@FrancescoPasa 2番目のスニペットは、「キーのみをチェックする必要がある場合」と明示的に言っています。{'a', 'b'}実際には{'a', 'b'};)のサブセットです
DylanYoung

19

完全を期すために、これを行うこともできます。

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

ただし、速度(またはその欠如)または読みやすさ(またはその欠如)については、何も主張しません。


補足:言及small.viewitems() <= big.viewitems()された他の回答は有望でしたが、注意点が1つあります。プログラムがPython 2.6(またはそれ以下)でも使用できる場合、d1.items() <= d2.items()実際には2つのタプルのリストを特定の順序なしで比較しているため、最終結果はおそらく信頼できません。そのため、@ blubberdiblubの回答に切り替えます。賛成。
RayLuo 2017

これはクールですが、ネストされた辞書では機能しないようです。
Frederik Baetens

@FrederikBaetensそれは意図されていません。また、それを行う方法は複数あり、そのような辞書に課す可能性のある複数の可能な構造/制限があるため、一般的に受け入れられている方法はないと思います。頭に浮かぶいくつかの質問を次に示します。より深い辞書を使用するかどうかをどのように決定しますか?dict基本クラスとして持つタイプのオブジェクトはどうですか?動作しておらず、まだ動作している場合はどうなりdictますか?まだdictのように動作する、一致するキーに異なるタイプの値が含まれている場合はどうsmallなりbigますか?
blubberdiblub

これらは有効なポイントですが、単純な入れ子になったdictで機能する基本的な機能は素晴らしいはずです。ここに例を投稿しましたが、@ NutCrackerのソリューションの方が優れています
Frederik Baetens

確かに、ネストされた辞書についての質問(および辞書の正確な要件が概説されていた)であれば、私はそれにひび割れがあったかもしれません。ポイントは、入れ子にされた辞書の解決策は、辞書がフラットな方法で別の辞書のサブ辞書であるかどうかを知りたいとき(つまりFalse、渡された辞書の値のときに厳密に答えが欲しいとき)には正しい答えを与えないということです一致するキーは異なります)。または言い換えると、ネストされたdictの解決策は、ユースケースによっては必ずしもドロップイン置換ではありません。
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

環境:

>>> 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. 2番目に直感的なのはsmall.viewitems() <= big.viewitems()、Python 2.7とsmall.items() <= big.items()Python 3 のみです。ただし、注意点が1つあります。バグがある可能性があります。プログラムがPython <= 2.6で使用される可能性がある場合、d1.items() <= d2.items()実際には2つのタプルのリストを特定の順序なしで比較しているため、最終結果は信頼できず、プログラムの厄介なバグになります。私は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


行内でリスト内に入れ子にされたdictを持つスーパーセットでコードが失敗する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
blubberdiblub

ありがとう、今すぐ修正する必要があります:)
Frederik Baetens
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.