web-dev-qa-db-fra.com

Python: vérifie si un dictionnaire est un sous-ensemble d'un autre dictionnaire plus volumineux

J'essaie d'écrire une méthode de filtrage personnalisée qui prend un nombre arbitraire de kwargs et renvoie une liste contenant les éléments d'une liste de type base de données contenant ces kwargs.

Par exemple, supposons que d1 = {'a':'2', 'b':'3'} et d2 = la même chose. d1 == d2 renvoie True. Mais supposons que d2 = la même chose plus un tas d’autres choses. Ma méthode doit pouvoir dire si d1 dans d2 , mais Python ne peut pas le faire avec les dictionnaires.

Le contexte:

J'ai une classe Word et chaque objet a des propriétés telles que Word, definition, part_of_speech et ainsi de suite. Je veux pouvoir appeler une méthode de filtrage dans la liste principale de ces mots, telle que Word.objects.filter(Word='jump', part_of_speech='verb-intransitive'). Je n'arrive pas à comprendre comment gérer ces clés et ces valeurs en même temps. Mais cela pourrait avoir une fonctionnalité plus large en dehors de ce contexte pour d'autres personnes.

70
Jamey

Convertir en paires d'éléments et vérifier le confinement.

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

L'optimisation est laissée comme un exercice pour le lecteur.

78

En Python 3, vous pouvez utiliser dict.items() pour obtenir une vue d'ensemble des éléments dict. Vous pouvez ensuite utiliser l'opérateur <= pour vérifier si une vue est un "sous-ensemble" de l'autre:

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

Dans Python 2.7, utilisez la dict.viewitems() pour faire la même chose:

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

Dans Python 2.6 et les versions ultérieures, vous aurez besoin d’une solution différente, telle que l’utilisation de all():

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

Remarque pour les personnes qui en ont besoin pour les tests unitaires: il existe également une méthode assertDictContainsSubset() dans la classe TestCase de Python.

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

Il est toutefois obsolète en 3.2, je ne sais pas pourquoi, il existe peut-être un remplaçant.

31
gitaarik

pour la vérification des clés et des valeurs, utilisez: set(d1.items()).issubset(set(d2.items()))

si vous ne devez vérifier que les touches: set(d1).issubset(set(d2))

20
kashchey

Pour être complet, vous pouvez aussi faire ceci:

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

Cependant, je ne fais aucune réclamation que ce soit concernant la vitesse (ou son absence) ou la lisibilité (ou son absence).

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

le contexte:

>>> 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
>>>
10
robert king

Ma fonction dans le même but, le faire de manière récursive:

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

Dans votre exemple, dictMatch(d1, d2) devrait renvoyer True même si d2 contient d'autres éléments, il s'applique également aux niveaux inférieurs:

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

dictMatch(d1, d2)   # True

Notes: Il pourrait y avoir une solution encore meilleure qui évite la clause if type(pvalue) is dict et s’applique à un éventail de cas encore plus large (comme des listes de hachages, etc.). De plus, la récursivité n'est pas limitée ici, utilisez-la à vos risques et périls. ;)

4
Alois Mahdal

Ce problème apparemment simple me coûte quelques heures en recherche pour trouver une solution fiable à 100%. J'ai donc documenté ce que j'ai trouvé dans cette réponse.

  1. "En pythonique", small_dict <= big_dict serait le moyen le plus intuitif, mais dommage qu'il soit ne fonctionnera pas. {'a': 1} < {'a': 1, 'b': 2} fonctionne apparemment en Python 2, mais il n’est pas fiable car la documentation officielle l’appelle explicitement. Lancer la recherche "Les résultats autres que l'égalité sont résolus de manière cohérente, mais ne sont pas définis autrement." dans cette section . Sans parler de la comparaison de 2 dict dans Python 3, on obtient une exception TypeError.

  2. La deuxième chose la plus intuitive est small.viewitems() <= big.viewitems() pour Python 2.7 uniquement et small.items() <= big.items() pour Python 3. Mais il y a une mise en garde: il s'agit de potentiellement bogué. Si votre programme peut potentiellement être utilisé sur Python <= 2.6, sa fonction d1.items() <= d2.items() compare en réalité 2 listes de n-uplets, sans ordre particulier, de sorte que le résultat final ne sera pas fiable et deviendra un bogue méchant dans votre programme. Je ne souhaite pas écrire encore une autre implémentation pour Python <= 2.6, mais je ne suis toujours pas sûr que mon code comporte un bogue connu (même s'il se trouve sur une plate-forme non prise en charge). Alors j'abandonne cette approche.

  3. Je m'installe avec la réponse de @blubberdiblub (le crédit lui revient):

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

    Il convient de noter que cette réponse repose sur le comportement == entre les dict, qui est clairement défini dans le document officiel, d'où devrait fonctionner dans toutes les versions de Python. Aller chercher:

    • "Les dictionnaires se comparent égaux si et seulement si ils ont les mêmes paires (clé, valeur)." est la dernière phrase de cette page
    • "Les mappages (instances de dict) comparent égal si et seulement si ils ont des paires égales (clé, valeur). La comparaison d'égalité des clés et des éléments applique la réflexivité." en cette page
3
RayLuo

Voici une solution générale récursive au problème donné:

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 __== "__main__":
    unittest.main()

NOTE: Le code original échouerait dans certains cas, les crédits pour la réparation va à @ olivier-melançon

2
BPL

Une courte implémentation récursive qui fonctionne pour les dictionnaires imbriqués:

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

Cela consommera les dits a et b. Si quelqu'un connaît un bon moyen d'éviter cela sans recourir à des solutions partiellement itératives comme dans d'autres réponses, dites-le-moi. J'aurais besoin d'un moyen de scinder un dict en tête et en queue en fonction d'une clé.

Ce code est plus utile comme exercice de programmation et est probablement beaucoup plus lent que d’autres solutions ici qui combinent récursivité et itération. La solution @ Nutcracker est très utile pour les dictionnaires imbriqués.

0
Frederik Baetens

Je sais que cette question est ancienne, mais voici ma solution pour vérifier si un dictionnaire imbriqué fait partie d'un autre dictionnaire imbriqué. La solution est récursive.

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
NutCracker

Si cela ne vous dérange pas d'utiliser pydash, il y a is_match qui fait exactement cela:

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






0
Zaro

Voici une solution qui revient aussi correctement dans les listes et les ensembles contenus dans le dictionnaire. Vous pouvez également l'utiliser pour des listes contenant des dictées, etc.

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
0
Frederik Baetens

Cette fonction fonctionne pour les valeurs non-hashable. Je pense aussi que c'est clair et facile à lire.

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
timthelion