Python: vérifiez si un dictionnaire est un sous-ensemble d'un autre dictionnaire plus grand

103

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 qui contient ces kwargs .

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

Le contexte:

J'ai une classe Word, et chaque objet possède des propriétés comme word, definition, part_of_speechet ainsi de suite. Je veux pouvoir appeler une méthode de filtrage sur la liste principale de ces mots, comme 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 des fonctionnalités plus importantes en dehors de ce contexte pour d'autres personnes.

Jamey
la source

Réponses:

109

Convertissez en paires d'articles et vérifiez le confinement.

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

L'optimisation reste un exercice pour le lecteur.

Ignacio Vazquez-Abrams
la source
18
Si les valeurs dict sont indexables, en utilisant viewitems () est la façon la plus optimizied je peux penser à: d1.viewitems() <= d2.viewitems(). Les exécutions Timeit ont montré une amélioration des performances de 3x. S'il n'est pas hachable, même utiliser iteritems()au lieu de items()conduit à une amélioration d'environ 1,2x. Cela a été fait en utilisant Python 2.7.
Chad
34
Je ne pense pas que l'optimisation devrait être laissée au lecteur - je crains que les gens l'utilisent sans se rendre compte que cela va créer une copie de superset.items () et l'itérer pour chaque élément du sous-ensemble.
robert king
4
Avec Python, 3 items()renverra des vues légères au lieu de copies. Aucune optimisation supplémentaire n'est nécessaire.
Kentzo
3
Qu'en est-il des répertoires imbriqués?
Andreas Profous
5
c'est hilarant. Je laisse le sujet raffinement de l'humour au lecteur.
deepelement
97

Dans 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 tester si une vue est un "sous-ensemble" de l'autre:

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

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

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

Dans Python 2.6 et les versions anté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)
augurar
la source
1
pour python3 cela devientd1.items() <= d2.items()
radu.ciorba
Attention: si votre programme pourrait potentiellement être utilisé sur Python 2.6 (ou même en dessous), d1.items() <= d2.items()ils comparent en fait 2 listes de tuples, sans ordre particulier, donc le résultat final ne sera probablement pas fiable. Pour cette raison, je passe à la réponse de @blubberdiblub.
RayLuo
1
d1.items() <= d2.items()est un comportement indéfini. Cela n'est pas documenté dans la documentation officielle et, le plus important, cela n'est pas testé: github.com/python/cpython/blob /... Cela dépend donc de l'implémentation.
Rodrigo Martins de Oliveira
2
@RodrigoMartins C'est documenté ici : "Pour les vues de type ensemble, toutes les opérations définies pour la classe de base abstraite collections.abc.Setsont disponibles"
augurar
1
@RodrigoMartins Si vous vous inquiétez pour les futurs responsables, enveloppez l'opération dans une fonction clairement nommée ou ajoutez un commentaire de code. Abaisser vos normes de code au niveau des développeurs incompétents est une mauvaise idée.
augurar
36

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

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

Il est cependant obsolète dans la version 3.2, je ne sais pas pourquoi, il y a peut-être un remplacement.

gitaarik
la source
29
était curieux, a trouvé ceci dans les nouveautés de 3.2 : La méthode assertDictContainsSubset () était obsolète car elle était mal implémentée avec les arguments dans le mauvais ordre. Cela a créé des illusions d'optique difficiles à déboguer où des tests comme TestCase (). AssertDictContainsSubset ({'a': 1, 'b': 2}, {'a': 1}) échouaient. (Contribution de Raymond Hettinger.)
Pedru
2
Attendez, le côté gauche est attendu et le côté droit est réel ... Cela ne devrait-il pas échouer? La seule chose qui ne va pas avec la fonction est celle que l'on va dans quelle place est déroutante?
JamesHutchison
21

pour les clés et les valeurs, vérifiez l'utilisation: set(d1.items()).issubset(set(d2.items()))

si vous devez vérifier uniquement les clés: set(d1).issubset(set(d2))

Kashchey
la source
11
La première expression ne fonctionnera pas si une valeur dans l'un des dictionnaires n'est pas hachable.
Pedro Romano
6
Le deuxième exemple peut être légèrement raccourci en supprimant l'ensemble (d2), car "issubset accepte tout itérable". docs.python.org/2/library/stdtypes.html#set
trojjer
C'est faux: d1={'a':1,'b':2}; d2={'a':2,'b':1}-> le deuxième extrait sera de retour True...
Francesco Pasa
1
@FrancescoPasa Le deuxième extrait de code dit explicitement: "si vous avez besoin de vérifier uniquement les clés". {'a', 'b'}est en fait un sous-ensemble de {'a', 'b'};)
DylanYoung
19

Pour être complet, vous pouvez également faire ceci:

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

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

blubberdiblub
la source
Une note latérale: d'autres réponses mentionnées small.viewitems() <= big.viewitems()étaient prometteuses, mais avec une mise en garde: si votre programme pouvait également être utilisé sur Python 2.6 (ou même en dessous), d1.items() <= d2.items()ils comparent en fait 2 listes de tuples, sans ordre particulier, donc le résultat final sera probablement pas fiable. Pour cette raison, je passe à la réponse de @blubberdiblub. J'ai voté pour.
RayLuo
C'est cool, mais cela ne semble pas fonctionner avec des dictionnaires imbriqués.
Frederik Baetens
@FrederikBaetens ce n'est pas censé le faire. De plus, je pense qu'il n'y a pas de moyen généralement accepté de le faire, car il y a plusieurs façons de procéder et il existe plusieurs structures / restrictions possibles que vous pourriez imposer à de tels dictionnaires. Voici quelques questions qui vous viennent à l'esprit: Comment déterminez-vous si un dictionnaire plus approfondi doit être descendu? Qu'en est-il des objets d'un type qui a dictcomme classe de base? Et s'il ne l'a pas fait et se comporte toujours comme un dict? Que faire si smallet bigcontenir des valeurs de type différent à une clé correspondante qui se comportent toujours comme dict?
blubberdiblub
Ce sont des points valides, mais une fonction de base fonctionnant avec des dictionnaires imbriqués simples devrait être agréable. J'ai posté un exemple ici , mais la solution de @ NutCracker est meilleure
Frederik Baetens
Bien sûr, s'il s'agissait d'une question sur les dictionnaires imbriqués (et si les exigences exactes pour les dictionnaires avaient été décrites), j'aurais peut-être eu une fissure. Le fait est qu'une solution pour les dictionnaires imbriqués ne donne pas la bonne réponse lorsque vous voulez savoir si un dict est un sous-dicté d'un autre de manière plate (c'est-à-dire lorsque vous voulez que la réponse soit strictement Falselorsque les valeurs des dictées passées sont différents pour les clés correspondantes). Ou en d'autres termes: la solution pour les dictionnaires imbriqués n'est pas nécessairement un remplacement instantané selon le cas d'utilisation.
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

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
>>>
Robert King
la source
4

Ma fonction dans le même but, en faisant cela 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 retourner True même si d2 contient d'autres éléments, et cela 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 if type(pvalue) is dictclause et s'applique à un éventail encore plus large de cas (comme les listes de hachages, etc.). De plus, la récursivité n'est pas limitée ici, alors utilisez-la à vos propres risques. ;)

Alois Mahdal
la source
4

Voici une solution qui se répète également correctement dans les listes et les ensembles contenus dans le dictionnaire. Vous pouvez également l'utiliser pour les listes contenant des dictionnaires, 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
Frederik Baetens
la source
2

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

  1. Parler "allié pythonique" small_dict <= big_dictserait la manière la plus intuitive, mais dommage que cela ne fonctionne pas . {'a': 1} < {'a': 1, 'b': 2}fonctionne apparemment dans Python 2, mais il n'est pas fiable car la documentation officielle le mentionne explicitement. Rechercher "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 oublier, la comparaison de 2 dicts en Python 3 entraîne une exception TypeError.

  2. La deuxième chose la plus intuitive small.viewitems() <= big.viewitems()concerne uniquement Python 2.7 et small.items() <= big.items()Python 3. Mais il y a une mise en garde: il est potentiellement bogué . Si votre programme peut potentiellement être utilisé sur Python <= 2.6, il d1.items() <= d2.items()compare en fait 2 listes de tuples, sans ordre particulier, donc le résultat final ne sera pas fiable et cela deviendra un vilain bogue dans votre programme. Je n'ai pas envie d'écrire une autre implémentation pour Python <= 2.6, mais je ne me sens toujours pas à l'aise avec le fait que mon code soit livré avec un bogue connu (même s'il se trouve sur une plate-forme non prise en charge). J'abandonne donc cette approche.

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

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

    Il convient de souligner que cette réponse repose sur le ==comportement entre les dictionnaires, qui est clairement défini dans le document officiel, et devrait donc fonctionner dans toutes les versions de Python . Lancer la recherche:

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

Voici une solution récursive générale pour le 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 __name__ == "__main__":
    unittest.main()

REMARQUE: le code d'origine échouerait dans certains cas, le crédit pour la correction va à @ olivier-melançon

BPL
la source
le code échoue avec un sur-ensemble qui a un dict imbriqué dans une liste, dans la ligneif not set(value) <= set(superset[key])
Eelco Hoogendoorn
2

Si cela ne vous dérange pas d'utiliser, pydash il y a is_matchlà 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
Zaro
la source
1

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
Casse Noisette
la source
0

Cette fonction fonctionne pour les valeurs non hachables. Je pense aussi qu'il 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
timthelion
la source
0

Une implémentation récursive courte 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 dicts 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, veuillez me le dire. J'aurais besoin d'un moyen de diviser un dict en tête et queue en fonction d'une clé.

Ce code est plus utile comme exercice de programmation, et est probablement beaucoup plus lent que les autres solutions ici qui mélangent récursivité et itération. La solution de Casse-Noisette est plutôt bonne pour les dictionnaires imbriqués.

Frederik Baetens
la source
1
Il manque quelque chose dans le code. Il descend simplement la première valeur commençant par a(et toute première valeur suivante) popitemtrouve. Il devrait également examiner d'autres éléments du même niveau. J'ai des paires de dictionnaires imbriqués où il renvoie la mauvaise réponse. (difficile de présenter un exemple à l'épreuve du temps ici, car il repose sur l'ordre de popitem)
blubberdiblub
Merci, devrait être corrigé maintenant :)
Frederik Baetens
0

Utilisez cet objet wrapper qui fournit une comparaison partielle et des différences intéressantes:


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
kolypto
la source