Python: controlla se un dizionario è un sottoinsieme di un altro dizionario più grande


100

Sto cercando di scrivere un metodo di filtro personalizzato che prenda un numero arbitrario di kwarg e restituisca un elenco contenente gli elementi di un elenco simile a un database che contengono quei kwarg .

Ad esempio, supponiamo d1 = {'a':'2', 'b':'3'}e d2= la stessa cosa. d1 == d2si traduce in True. Ma supponiamo d2= la stessa cosa più un mucchio di altre cose. Il mio metodo deve essere in grado di dire se d1 in d2 , ma Python non può farlo con i dizionari.

Contesto:

Ho una classe di Word, e ogni oggetto ha proprietà come word, definition, part_of_speeche così via. Voglio essere in grado di chiamare un metodo di filtro nell'elenco principale di queste parole, come Word.objects.filter(word='jump', part_of_speech='verb-intransitive'). Non riesco a capire come gestire queste chiavi e valori contemporaneamente. Ma questo potrebbe avere funzionalità più ampie al di fuori di questo contesto per altre persone.

Risposte:


108

Converti in coppie di oggetti e verifica il contenimento.

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

L'ottimizzazione è lasciata come esercizio per il lettore.


18
Se i valori sono dict hashable, utilizzando viewitems () è il modo più optimizied mi viene in mente: d1.viewitems() <= d2.viewitems(). Le corse di Timeit hanno mostrato un miglioramento delle prestazioni di 3 volte. Se non è modificabile, anche l'uso al iteritems()posto di items()porta a un miglioramento di circa 1,2 volte. Ciò è stato fatto utilizzando Python 2.7.
Ciad

34
Non penso che l'ottimizzazione debba essere lasciata al lettore - sono preoccupato che le persone lo useranno effettivamente senza rendersi conto che creerà una copia di superset.items () e itererà attraverso di essa per ogni elemento nel sottoinsieme.
robert king

4
Con Python 3 items()restituirà visualizzazioni leggere anziché copie. Non sono necessarie ulteriori ottimizzazioni.
Kentzo,

3
E le directory annidate?
Andreas Profous

5
questo è divertente. Lascio al lettore la raffinatezza del soggetto dell'umorismo.
deepelement

95

In Python 3, puoi usare dict.items()per ottenere una vista simile a un set degli elementi del dict. È quindi possibile utilizzare l' <=operatore per verificare se una vista è un "sottoinsieme" dell'altra:

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

In Python 2.7, usa dict.viewitems()per fare lo stesso:

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

In Python 2.6 e versioni precedenti avrai bisogno di una soluzione diversa, come l'utilizzo di all():

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

1
per python3 questo diventad1.items() <= d2.items()
radu.ciorba

Avvertenza: se il tuo programma potrebbe essere potenzialmente utilizzato su Python 2.6 (o anche sotto), d1.items() <= d2.items()stanno effettivamente confrontando 2 elenchi di tuple, senza un ordine particolare, quindi il risultato finale probabilmente non sarà affidabile. Per questo motivo passo alla risposta di @blubberdiblub.
RayLuo

1
d1.items() <= d2.items()è un comportamento indefinito. Non è documentato nei documenti ufficiali e, cosa più importante, non è testato: github.com/python/cpython/blob/… Quindi questo dipende dall'implementazione.
Rodrigo Martins de Oliveira

2
@RodrigoMartins È documentato qui : "Per le viste tipo set, tutte le operazioni definite per la classe base astratta collections.abc.Setsono disponibili"
augurar

1
@RodrigoMartins Se sei preoccupato per i futuri manutentori, racchiudi l'operazione in una funzione chiaramente denominata o aggiungi un commento sul codice. Abbassare gli standard del codice al livello di sviluppatori incompetenti è un'idea terribile.
augurar

36

Nota per le persone che ne hanno bisogno per i test unitari: c'è anche un assertDictContainsSubset()metodo nella TestCaseclasse di Python .

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

Tuttavia è deprecato in 3.2, non so perché, forse c'è un sostituto per esso.


29
era curioso, l'ho trovato nelle novità della 3.2 : Il metodo assertDictContainsSubset () è stato deprecato perché è stato implementato male con gli argomenti nell'ordine sbagliato. Questo ha creato illusioni ottiche difficili da eseguire il debug in cui test come TestCase (). AssertDictContainsSubset ({'a': 1, 'b': 2}, {'a': 1}) fallivano. (Contributo di Raymond Hettinger.)
Pedru

2
Aspetta, il lato sinistro è previsto e il lato destro è reale ... Non dovrebbe fallire? L'unica cosa sbagliata con la funzione è quella che va in quale posto è confuso?
JamesHutchison

21

per chiavi e valori controllare utilizzare: set(d1.items()).issubset(set(d2.items()))

se devi controllare solo le chiavi: set(d1).issubset(set(d2))


11
La prima espressione non funzionerà se qualsiasi valore in uno dei dizionari non è hash.
Pedro Romano

6
Il secondo esempio può essere leggermente accorciato rimuovendo l'insieme (d2), poiché "issubset accetta qualsiasi iterabile". docs.python.org/2/library/stdtypes.html#set
trojjer

Questo è sbagliato: d1={'a':1,'b':2}; d2={'a':2,'b':1}-> tornerà il secondo snippet True...
Francesco Pasa

1
@FrancescoPasa Il secondo snippet dice esplicitamente: "se devi controllare solo le chiavi". {'a', 'b'}è infatti un sottoinsieme di {'a', 'b'};)
DylanYoung

19

Per completezza, puoi anche fare questo:

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

Tuttavia, non ho alcuna pretesa in merito alla velocità (o mancanza di ciò) o alla leggibilità (o alla sua mancanza).


Una nota a margine: altre risposte menzionate small.viewitems() <= big.viewitems()erano promettenti, ma con un avvertimento: se il tuo programma potesse essere utilizzato anche su Python 2.6 (o anche sotto), d1.items() <= d2.items()stanno effettivamente confrontando 2 elenchi di tuple, senza un ordine particolare, quindi il risultato finale sarà probabilmente non affidabile. Per questo motivo, passo alla risposta di @blubberdiblub. Votato.
RayLuo

È interessante, ma non sembra funzionare con dizionari annidati.
Frederik Baetens

@FrederikBaetens non è destinato. Inoltre, credo che non ci sia un modo generalmente accettato come farlo, perché ci sono molti modi in cui potresti farlo e ci sono più possibili strutture / restrizioni che potresti imporre a tali dizionari. Ecco alcune domande che vengono in mente: come si determina se si debba discendere in un dizionario più profondo? E gli oggetti di un tipo che ha dictcome classe base? E se non lo ha fatto e si comporta ancora come un dict? Cosa succede se smalle bigcontengono valori di tipo diverso in una chiave di corrispondenza che si comportano ancora come dict?
blubberdiblub

Questi sono punti validi, ma una funzione di base che ha funzionato con semplici dict annidati dovrebbe essere carina. Ho pubblicato un esempio qui , ma la soluzione di @ NutCracker è migliore
Frederik Baetens

Certo, se fosse stata una domanda sui dizionari annidati (e se fossero stati delineati i requisiti esatti per i dizionari), forse avrei avuto un problema. Il punto è che una soluzione per i dizionari annidati non dà la risposta giusta quando vuoi sapere se un dict è un sottodict di un altro in modo piatto (cioè quando vuoi che la risposta sia rigorosamente Falsequando i valori dei dict passati sono diversi per le chiavi corrispondenti). O in altre parole: la soluzione per i dict annidati non è necessariamente una sostituzione immediata a seconda del caso d'uso.
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

contesto:

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

La mia funzione per lo stesso scopo, facendo ciò in modo ricorsivo:

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

Nel tuo esempio, dictMatch(d1, d2)dovrebbe restituire True anche se d2 contiene altre cose, inoltre si applica anche ai livelli inferiori:

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

dictMatch(d1, d2)   # True

Note: potrebbe esserci una soluzione ancora migliore che evita la if type(pvalue) is dictclausola e si applica a una gamma ancora più ampia di casi (come elenchi di hash ecc.). Anche la ricorsione non è limitata qui, quindi usala a tuo rischio. ;)


4

Ecco una soluzione che ricorre correttamente anche in elenchi e insiemi contenuti nel dizionario. Puoi anche usarlo per elenchi contenenti dict ecc ...

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

Questo problema apparentemente semplice mi costa un paio d'ore di ricerca per trovare una soluzione affidabile al 100%, quindi ho documentato ciò che ho trovato in questa risposta.

  1. Parlare "alleato pitonico" small_dict <= big_dictsarebbe il modo più intuitivo, ma peccato che non funzioni . {'a': 1} < {'a': 1, 'b': 2}apparentemente funziona in Python 2, ma non è affidabile perché la documentazione ufficiale lo chiama esplicitamente. Vai a cercare "I risultati diversi dall'uguaglianza vengono risolti in modo coerente, ma non sono altrimenti definiti". in questa sezione . Per non parlare del fatto che il confronto di 2 dict in Python 3 produce un'eccezione TypeError.

  2. La seconda cosa più intuitiva è solo small.viewitems() <= big.viewitems()per Python 2.7 e small.items() <= big.items()per Python 3. Ma c'è un avvertimento: è potenzialmente difettoso . Se il tuo programma può essere potenzialmente utilizzato su Python <= 2.6, d1.items() <= d2.items()sta effettivamente confrontando 2 elenchi di tuple, senza un ordine particolare, quindi il risultato finale sarà inaffidabile e diventerà un brutto bug nel tuo programma. Non mi piace scrivere ancora un'altra implementazione per Python <= 2.6, ma ancora non mi sento a mio agio che il mio codice venga fornito con un bug noto (anche se si trova su una piattaforma non supportata). Quindi abbandono questo approccio.

  3. Mi sistemo con la risposta di @blubberdiblub (il merito va a lui):

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

    Vale la pena sottolineare che questa risposta si basa sul ==comportamento tra i dict, che è chiaramente definito nel documento ufficiale, quindi dovrebbe funzionare in ogni versione di Python . Vai a cercare:

    • "I dizionari confrontano uguali se e solo se hanno le stesse coppie (chiave, valore)". è l'ultima frase in questa pagina
    • "Le mappature (istanze di dict) confrontano uguali se e solo se hanno coppie uguali (chiave, valore). Il confronto dell'uguaglianza delle chiavi e degli elementi rafforza la riflessività." in questa pagina

2

Ecco una soluzione ricorsiva generale per il problema dato:

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

NOTA: il codice originale non funzionerebbe in alcuni casi, i crediti per la correzione vanno a @ olivier-melançon


il codice fallisce con un superset che ha un dict annidato all'interno di un elenco, nella rigaif not set(value) <= set(superset[key])
Eelco Hoogendoorn

2

Se non ti dispiace utilizzando pydash v'è is_matchlì che fa esattamente questo:

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

So che questa domanda è vecchia, ma ecco la mia soluzione per verificare se un dizionario annidato fa parte di un altro dizionario annidato. La soluzione è ricorsiva.

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

Questa funzione funziona per i valori non modificabili. Penso anche che sia chiaro e di facile lettura.

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

Una breve implementazione ricorsiva che funziona per dizionari annidati:

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

Questo consumerà i dettami aeb. Se qualcuno conosce un buon modo per evitarlo senza ricorrere a soluzioni parzialmente iterative come in altre risposte, per favore dimmelo. Avrei bisogno di un modo per dividere un dict in testa e coda in base a una chiave.

Questo codice è più utile come esercizio di programmazione e probabilmente è molto più lento di altre soluzioni qui che mescolano ricorsione e iterazione. La soluzione di @ Nutcracker è abbastanza buona per i dizionari annidati.


1
C'è qualcosa che manca nel codice. Scende solo il primo valore a partire da a(e qualsiasi primo valore successivo) popitemtrova. Dovrebbe anche esaminare altri elementi sullo stesso livello. Ho coppie di dict annidati in cui restituisce la risposta sbagliata. (difficile presentare un esempio a prova di futuro qui, poiché si basa sull'ordine di popitem)
blubberdiblub

Grazie, dovrebbe essere risolto ora :)
Frederik Baetens

0

Usa questo oggetto wrapper che fornisce un confronto parziale e buone differenze:


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
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.