Python: Überprüfen Sie, ob ein Wörterbuch eine Teilmenge eines anderen größeren Wörterbuchs ist

100

Ich versuche, eine benutzerdefinierte Filtermethode zu schreiben, die eine beliebige Anzahl von kwargs verwendet und eine Liste zurückgibt, die die Elemente einer datenbankähnlichen Liste enthält, die diese kwargs enthält .

Nehmen wir zum Beispiel an d1 = {'a':'2', 'b':'3'}und d2= dasselbe. d1 == d2führt zu True. Aber nehmen wir an d2= dasselbe plus eine Reihe anderer Dinge. Meine Methode muss in der Lage sein zu erkennen, ob d1 in d2 ist , aber Python kann das nicht mit Wörterbüchern tun.

Kontext:

Ich habe eine Wortklasse, und jedes Objekt hat Eigenschaften wie word, definition, part_of_speech, und so weiter. Ich möchte in der Lage sein, eine Filtermethode in der Hauptliste dieser Wörter aufzurufen, wie z Word.objects.filter(word='jump', part_of_speech='verb-intransitive'). Ich kann nicht herausfinden, wie diese Schlüssel und Werte gleichzeitig verwaltet werden. Dies könnte jedoch außerhalb dieses Kontexts eine größere Funktionalität für andere Personen haben.

Jamey
quelle

Antworten:

108

In Artikelpaare konvertieren und auf Eindämmung prüfen.

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

Die Optimierung bleibt dem Leser als Übung überlassen.

Ignacio Vazquez-Abrams
quelle
17
Wenn die Diktatwerte hashbar sind, ist die Verwendung von viewitems () die optimierteste Methode, die ich mir vorstellen kann : d1.viewitems() <= d2.viewitems(). Timeit-Läufe zeigten eine dreifache Leistungsverbesserung. Wenn nicht hashbar, führt selbst die Verwendung iteritems()anstelle von items()zu einer etwa 1,2-fachen Verbesserung. Dies wurde mit Python 2.7 durchgeführt.
Chad
33
Ich denke nicht, dass die Optimierung dem Leser überlassen werden sollte - ich bin besorgt, dass die Leute dies tatsächlich verwenden werden, ohne zu bemerken, dass es eine Kopie von superset.items () erstellen und für jedes Element in der Teilmenge durchlaufen wird.
Robert King
4
Mit Python 3 items()werden einfache Ansichten anstelle von Kopien zurückgegeben. Es ist keine weitere Optimierung erforderlich.
Kentzo
3
Was ist mit verschachtelten Verzeichnissen?
Andreas Profous
4
Das ist urkomisch. Ich überlasse die Verfeinerung des Humors dem Leser.
deepelement
95

In Python 3 können Sie dict.items()eine satzartige Ansicht der diktierten Elemente erhalten. Mit dem <=Operator können Sie dann testen, ob eine Ansicht eine "Teilmenge" der anderen ist:

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

Verwenden Sie in Python 2.7 Folgendes, um dict.viewitems()dasselbe zu tun:

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

In Python 2.6 und niedriger benötigen Sie eine andere Lösung, z. B all().:

all(key in d2 and d2[key] == d1[key] for key in d1)
Augurar
quelle
1
für Python3 wird diesd1.items() <= d2.items()
radu.ciorba
Vorsichtsmaßnahme: Wenn Ihr Programm möglicherweise unter Python 2.6 (oder sogar darunter) verwendet werden könnte, d1.items() <= d2.items()werden tatsächlich zwei Tupellisten ohne bestimmte Reihenfolge verglichen, sodass das Endergebnis wahrscheinlich nicht zuverlässig ist. Aus diesem Grund wechsle ich zu @blubberdiblubs Antwort.
RayLuo
1
d1.items() <= d2.items()ist undefiniertes Verhalten. Es ist nicht in den offiziellen Dokumenten dokumentiert und wird vor allem nicht getestet: github.com/python/cpython/blob/… Dies ist also implementierungsabhängig.
Rodrigo Martins de Oliveira
2
@RodrigoMartins Es ist hier dokumentiert : "Für satzartige Ansichten sind alle für die abstrakte Basisklasse definierten Operationen collections.abc.Setverfügbar"
August
1
@RodrigoMartins Wenn Sie sich Sorgen um zukünftige Betreuer machen, schließen Sie den Vorgang in eine klar benannte Funktion ein oder fügen Sie einen Codekommentar hinzu. Es ist eine schreckliche Idee, Ihre Codestandards auf das Niveau inkompetenter Entwickler zu senken.
August
36

Hinweis für Personen, die dies für Unit-Tests benötigen: Es gibt auch eine assertDictContainsSubset()Methode in Pythons TestCaseKlasse.

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

Es ist jedoch in 3.2 veraltet, nicht sicher warum, vielleicht gibt es einen Ersatz dafür.

Gitaarik
quelle
29
war neugierig, fand dies in dem, was in 3.2 neu ist : Die assertDictContainsSubset () -Methode wurde veraltet, weil sie mit den Argumenten in der falschen Reihenfolge falsch implementiert wurde. Dies führte zu schwer zu debuggenden optischen Täuschungen, bei denen Tests wie TestCase (). AssertDictContainsSubset ({'a': 1, 'b': 2}, {'a': 1}) fehlschlagen würden. (Beigetragen von Raymond Hettinger.)
Pedru
2
Warten Sie, die linke Seite wird erwartet und die rechte Seite ist aktuell ... Sollte das nicht scheitern? Das einzige, was an der Funktion falsch ist, ist, dass das, was an welchen Ort geht, verwirrend ist?
James Hutchison
21

Für Schlüssel und Werte überprüfen Sie Folgendes: set(d1.items()).issubset(set(d2.items()))

Wenn Sie nur die Schlüssel überprüfen müssen: set(d1).issubset(set(d2))

kashchey
quelle
11
Der erste Ausdruck funktioniert nicht, wenn ein Wert in einem der Wörterbücher nicht hashbar ist.
Pedro Romano
6
Das zweite Beispiel kann durch Entfernen der Menge (d2) leicht verkürzt werden, da "issubset jede iterable akzeptiert". docs.python.org/2/library/stdtypes.html#set
trojjer
Das ist falsch: d1={'a':1,'b':2}; d2={'a':2,'b':1}-> das zweite Snippet wird zurückkehren True...
Francesco Pasa
1
@FrancescoPasa Das zweite Snippet sagt explizit: "Wenn Sie nur Schlüssel überprüfen müssen". {'a', 'b'}ist in der Tat eine Teilmenge von {'a', 'b'};)
DylanYoung
19

Der Vollständigkeit halber können Sie auch Folgendes tun:

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

Ich mache jedoch keinerlei Ansprüche hinsichtlich Geschwindigkeit (oder mangelnder Geschwindigkeit) oder Lesbarkeit (oder mangelnder Lesbarkeit).

blubberdiblub
quelle
Eine Randnotiz: Andere Antworten small.viewitems() <= big.viewitems()waren vielversprechend, aber mit einer Einschränkung: Wenn Ihr Programm auch unter Python 2.6 (oder sogar darunter) verwendet werden könnte, d1.items() <= d2.items()werden tatsächlich zwei Listen von Tupeln ohne bestimmte Reihenfolge verglichen, sodass das Endergebnis wahrscheinlich sein wird nicht zuverlässig. Aus diesem Grund wechsle ich zu @blubberdiblubs Antwort. Upvoted.
RayLuo
Das ist cool, aber es scheint nicht mit verschachtelten Wörterbüchern zu funktionieren.
Frederik Baetens
@FrederikBaetens ist es nicht gedacht. Ich glaube auch, dass es keinen allgemein akzeptierten Weg gibt, dies zu tun, da es mehrere Möglichkeiten gibt, wie Sie vorgehen können, und es gibt mehrere mögliche Strukturen / Einschränkungen, die Sie solchen Wörterbüchern auferlegen könnten. Hier sind einige Fragen, die mir in den Sinn kommen: Wie bestimmen Sie, ob ein tieferes Wörterbuch verwendet werden soll? Was ist mit Objekten eines Typs, der dictals Basisklasse dient? Was ist, wenn es sich nicht wie ein verhält dict? Was ist, wenn smallund bigWerte unterschiedlichen Typs an einem passenden Schlüssel enthalten, die sich immer noch wie diktieren verhalten?
Blubberdiblub
Das sind gültige Punkte, aber eine Grundfunktion, die mit einfach verschachtelten Diktaten funktioniert, sollte nett sein. Ich habe hier ein Beispiel gepostet , aber @ NutCrackers Lösung ist besser
Frederik Baetens
Wäre es eine Frage zu verschachtelten Wörterbüchern gewesen (und wären die genauen Anforderungen für die Wörterbücher dargelegt worden), hätte ich vielleicht einen Riss gehabt. Der Punkt ist, dass eine Lösung für verschachtelte Wörterbücher nicht die richtige Antwort liefert, wenn Sie wissen möchten, ob ein Diktat auf flache Weise ein Unterdikt eines anderen ist (dh wenn Sie möchten, dass die Antwort ausschließlich Falseden Werten der übergebenen Diktate entspricht sind unterschiedlich für passende Schlüssel). Oder mit anderen Worten: Die Lösung für verschachtelte Dikte ist nicht unbedingt ein Drop-In-Ersatz, abhängig vom Anwendungsfall.
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

Kontext:

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

Meine Funktion für den gleichen Zweck, rekursiv:

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

In Ihrem Beispiel dictMatch(d1, d2)sollte True zurückgegeben werden, auch wenn d2 andere Inhalte enthält. Außerdem gilt dies auch für niedrigere Ebenen:

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

dictMatch(d1, d2)   # True

Anmerkungen: Es könnte eine noch bessere Lösung geben, die die if type(pvalue) is dictKlausel vermeidet und für eine noch größere Bandbreite von Fällen gilt (z. B. Listen von Hashes usw.). Auch die Rekursion ist hier nicht beschränkt. Verwenden Sie sie daher auf eigenes Risiko. ;)

Alois Mahdal
quelle
4

Hier ist eine Lösung, die auch in Listen und Mengen, die im Wörterbuch enthalten sind, ordnungsgemäß wiederholt wird. Sie können dies auch für Listen verwenden, die Diktate usw. enthalten.

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

Dieses scheinbar unkomplizierte Problem kostet mich ein paar Stunden Recherche, um eine 100% zuverlässige Lösung zu finden. Deshalb habe ich dokumentiert, was ich in dieser Antwort gefunden habe.

  1. "Pythonic-Verbündeter" zu sprechen, small_dict <= big_dictwäre der intuitivste Weg, aber schade, dass es nicht funktioniert . {'a': 1} < {'a': 1, 'b': 2}funktioniert anscheinend in Python 2, ist aber nicht zuverlässig, da die offizielle Dokumentation dies ausdrücklich anzeigt. Suche suchen "Andere Ergebnisse als Gleichheit werden konsistent aufgelöst, aber nicht anders definiert." in diesem Abschnitt . Ganz zu schweigen davon, dass der Vergleich von 2 Dikten in Python 3 zu einer TypeError-Ausnahme führt.

  2. Die zweitintuitivste Sache ist nur small.viewitems() <= big.viewitems()für Python 2.7 und small.items() <= big.items()für Python 3. Aber es gibt eine Einschränkung: Es ist möglicherweise fehlerhaft . Wenn Ihr Programm möglicherweise unter Python <= 2.6 verwendet werden könnte, d1.items() <= d2.items()werden tatsächlich zwei Tupellisten ohne bestimmte Reihenfolge verglichen, sodass das Endergebnis unzuverlässig ist und zu einem bösen Fehler in Ihrem Programm wird. Ich bin nicht daran interessiert, eine weitere Implementierung für Python <= 2.6 zu schreiben, aber ich fühle mich immer noch nicht wohl, dass mein Code einen bekannten Fehler enthält (selbst wenn er sich auf einer nicht unterstützten Plattform befindet). Also gebe ich diesen Ansatz auf.

  3. Ich beruhige mich mit der Antwort von @blubberdiblub (Gutschrift geht an ihn):

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

    Es sei darauf hingewiesen, dass diese Antwort auf dem ==Verhalten zwischen Diktaten beruht , das im offiziellen Dokument klar definiert ist und daher in jeder Python-Version funktionieren sollte . Suche suchen:

    • "Wörterbücher werden genau dann gleich verglichen, wenn sie dieselben (Schlüssel-, Wert-) Paare haben." ist der letzte Satz auf dieser Seite
    • "Zuordnungen (Instanzen von Diktaten) werden genau dann gleich verglichen, wenn sie gleiche (Schlüssel-, Wert-) Paare haben. Ein Gleichheitsvergleich der Schlüssel und Elemente erzwingt Reflexivität." auf dieser Seite
RayLuo
quelle
2

Hier ist eine allgemeine rekursive Lösung für das gegebene Problem:

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

HINWEIS: Der ursprüngliche Code in bestimmten Fällen fehlschlagen würde, Kredite für die Befestigung geht an @ olivier-Melançon

BPL
quelle
Code schlägt mit einer Obermenge fehl, die ein Diktat in einer Liste in der Zeile verschachtelt hatif not set(value) <= set(superset[key])
Eelco Hoogendoorn
2

Wenn es Ihnen nichts ausmacht, pydash gibt es is_matchdort, was genau das tut:

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

Ich weiß, dass diese Frage alt ist, aber hier ist meine Lösung, um zu überprüfen, ob ein verschachteltes Wörterbuch Teil eines anderen verschachtelten Wörterbuchs ist. Die Lösung ist rekursiv.

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
Nussknacker
quelle
0

Diese Funktion funktioniert für nicht hashbare Werte. Ich denke auch, dass es klar und leicht zu lesen ist.

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

Eine kurze rekursive Implementierung, die für verschachtelte Wörterbücher funktioniert:

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

Dies verbraucht die a- und b-Wörter. Wenn jemand einen guten Weg kennt, dies zu vermeiden, ohne auf teilweise iterative Lösungen wie in anderen Antworten zurückzugreifen, sagen Sie es mir bitte. Ich würde einen Weg brauchen, um ein Diktat basierend auf einem Schlüssel in Kopf und Schwanz aufzuteilen.

Dieser Code ist als Programmierübung nützlicher und wahrscheinlich viel langsamer als andere Lösungen, die Rekursion und Iteration mischen. Die Lösung von @ Nutcracker ist ziemlich gut für verschachtelte Wörterbücher.

Frederik Baetens
quelle
1
Im Code fehlt etwas. Es wird nur der erste Wert abwärts ab a(und alle nachfolgenden ersten Werte) gefunden popitem. Es sollte auch andere Elemente auf derselben Ebene untersuchen. Ich habe zwei verschachtelte Wörter, bei denen die falsche Antwort zurückgegeben wird. (schwer ein zukunftssicheres Beispiel hier zu präsentieren, da es auf der Reihenfolge von beruht popitem)
blubberdiblub
Danke, sollte jetzt behoben werden :)
Frederik Baetens