Python: verifique se um dicionário é um subconjunto de outro dicionário maior

100

Estou tentando escrever um método de filtro personalizado que usa um número arbitrário de kwargs e retorna uma lista contendo os elementos de uma lista do tipo banco de dados que contém esses kwargs .

Por exemplo, suponha d1 = {'a':'2', 'b':'3'}e d2= a mesma coisa. d1 == d2resulta em Verdadeiro. Mas suponha d2= a mesma coisa mais um monte de outras coisas. Meu método precisa ser capaz de dizer se d1 em d2 , mas Python não pode fazer isso com dicionários.

Contexto:

Eu tenho uma classe Word, e cada objeto tem propriedades como word, definition, part_of_speeche assim por diante. Quero ser capaz de chamar um método de filtro na lista principal dessas palavras, como Word.objects.filter(word='jump', part_of_speech='verb-intransitive'). Não consigo descobrir como gerenciar essas chaves e valores ao mesmo tempo. Mas isso poderia ter uma funcionalidade maior fora deste contexto para outras pessoas.

Jamey
fonte

Respostas:

108

Converta em pares de itens e verifique se há contenção.

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

A otimização é deixada como um exercício para o leitor.

Ignacio Vazquez-Abrams
fonte
18
Se os valores de dicionários são Hashable, usando viewitems () é a forma mais optimizied eu posso pensar de: d1.viewitems() <= d2.viewitems(). A execução do Timeit mostrou uma melhoria de desempenho de mais de 3x. Se não for hashable, mesmo usando em iteritems()vez de items()leva a uma melhoria de cerca de 1,2x. Isso foi feito usando Python 2.7.
Chad
34
não acho que a otimização deva ser deixada para o leitor - estou preocupado que as pessoas realmente usem isso sem perceber que vai construir uma cópia do superset.items () e iterar por meio dele para cada item do subconjunto.
robert king
4
Com Python 3 items()retornará visualizações leves em vez de cópias. Nenhuma otimização adicional é necessária.
Kentzo
3
E os diretórios aninhados?
Andreas Profous
5
isso é hilário. Vou deixar o assunto refinamento do humor para o leitor.
deepelement
95

No Python 3, você pode usar dict.items()para obter uma visão semelhante a um conjunto dos itens de dicionário. Você pode então usar o <=operador para testar se uma visualização é um "subconjunto" da outra:

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

No Python 2.7, use o dict.viewitems()para fazer o mesmo:

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

No Python 2.6 e abaixo, você precisará de uma solução diferente, como usar all():

all(key in d2 and d2[key] == d1[key] for key in d1)
augurar
fonte
1
para python3 tornad1.items() <= d2.items()
radu.ciorba
Advertência: se o seu programa puder ser potencialmente usado no Python 2.6 (ou mesmo abaixo), eles d1.items() <= d2.items()estão, na verdade, comparando 2 listas de tuplas, sem uma ordem particular, então o resultado final provavelmente não será confiável. Por esse motivo, mudo para a resposta de @blubberdiblub.
RayLuo de
1
d1.items() <= d2.items()é um comportamento indefinido. Isso não está documentado nos documentos oficiais e, o mais importante, não foi testado: github.com/python/cpython/blob/… Portanto, isso depende da implementação.
Rodrigo Martins de Oliveira
2
@RodrigoMartins Está documentado aqui : "Para visualizações em conjunto, todas as operações definidas para a classe base abstrata collections.abc.Setestão disponíveis"
augurar
1
@RodrigoMartins Se você está preocupado com futuros mantenedores, envolva a operação em uma função claramente nomeada ou adicione um comentário de código. Reduzir seus padrões de código ao nível de desenvolvedores incompetentes é uma ideia terrível.
augurar
36

Nota para as pessoas que precisam disso para testes de unidade: também há um assertDictContainsSubset()método na TestCaseclasse do Python .

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

No entanto, ele está obsoleto no 3.2, não sei por que, talvez haja um substituto para ele.

gitaarik
fonte
29
estava curioso, encontrei isso no que há de novo no 3.2 : O método assertDictContainsSubset () foi descontinuado porque foi mal implementado com os argumentos na ordem errada. Isso criou ilusões ópticas difíceis de depurar, nas quais testes como TestCase (). AssertDictContainsSubset ({'a': 1, 'b': 2}, {'a': 1}) falhariam. (Contribuição de Raymond Hettinger.)
Pedru
2
Espere, o lado esquerdo é esperado e o lado direito é real ... Isso não deveria falhar? A única coisa errada com a função é aquela em que lugar fica confuso?
JamesHutchison 01 de
21

para chaves e valores, verifique o uso: set(d1.items()).issubset(set(d2.items()))

se você precisar verificar apenas as chaves: set(d1).issubset(set(d2))

Kashchey
fonte
11
A primeira expressão não funcionará se algum valor em qualquer um dos dicionários não for hashble.
Pedro Romano
6
O segundo exemplo pode ser abreviado ligeiramente removendo o conjunto (d2), pois "issubset aceita qualquer iterável". docs.python.org/2/library/stdtypes.html#set
trojjer
Isto está errado: d1={'a':1,'b':2}; d2={'a':2,'b':1}-> o segundo fragmento retornará True...
Francesco Pasa
1
@FrancescoPasa O segundo trecho diz explicitamente: "se você precisar verificar apenas as chaves". {'a', 'b'}é na verdade um subconjunto de {'a', 'b'};)
DylanYoung
19

Para ser completo, você também pode fazer isso:

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

No entanto, eu não faço qualquer reclamação sobre velocidade (ou falta dela) ou legibilidade (ou falta dela).

blubberdiblub
fonte
Uma nota lateral: outras respostas mencionando small.viewitems() <= big.viewitems()foram promissoras, mas com uma ressalva: se seu programa também pudesse ser usado no Python 2.6 (ou mesmo abaixo), eles d1.items() <= d2.items()estão, na verdade, comparando 2 listas de tuplas, sem ordem particular, então o resultado final provavelmente não confiável. Por esse motivo, mudo para a resposta de @blubberdiblub. Votado.
RayLuo de
Isso é legal, mas não parece funcionar com dicionários aninhados.
Frederik Baetens
@FrederikBaetens não é para isso. Além disso, acredito que não haja uma maneira geralmente aceita de fazer isso, porque há várias maneiras de fazer isso e várias estruturas / restrições possíveis que você pode impor a esses dicionários. Aqui estão algumas perguntas que vêm à mente: Como você determina se um dicionário mais profundo deve ser acessado? E quanto aos objetos de um tipo que tem dictcomo classe base? E se não tiver feito isso e ainda se comportar como um dict? E se smalle bigcontiver valores de tipos diferentes em uma chave correspondente que ainda se comportam como dict?
blubberdiblub
Esses são pontos válidos, mas uma função básica que funcionava com dicts aninhados simples deve ser boa. Postei um exemplo aqui , mas a solução do @NutCracker é melhor
Frederik Baetens
Claro, se fosse uma pergunta sobre dicionários aninhados (e se os requisitos exatos para os dicionários estivessem descritos), eu poderia ter tentado resolvê-la. A questão é que uma solução para dicionários aninhados não dá a resposta certa quando você quer saber se um dicionário é uma subdicção de outro de forma plana (ou seja, quando você quer que a resposta seja estritamente Falsequando os valores dos ditos passados são diferentes para chaves correspondentes). Ou em outras palavras: a solução para dicts aninhados não é necessariamente uma substituição imediata, dependendo do caso de 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

contexto:

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

Minha função com o mesmo propósito, fazendo isso recursivamente:

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

Em seu exemplo, dictMatch(d1, d2)deve retornar True mesmo se d2 tiver outras coisas nele, além de se aplicar também a níveis inferiores:

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

dictMatch(d1, d2)   # True

Notas: Pode haver uma solução ainda melhor que evite a if type(pvalue) is dictcláusula e se aplique a uma gama ainda maior de casos (como listas de hashes, etc.). Além disso, a recursão não é limitada aqui, portanto, use por sua própria conta e risco. ;)

Alois Mahdal
fonte
4

Aqui está uma solução que também recorre adequadamente às listas e conjuntos contidos no dicionário. Você também pode usar isso para listas contendo dictos 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
fonte
2

Esse problema aparentemente simples me custa algumas horas em pesquisas para encontrar uma solução 100% confiável, então documentei o que encontrei nesta resposta.

  1. Falando de forma "pitônica", small_dict <= big_dictseria a forma mais intuitiva, mas uma pena que não funcionará . {'a': 1} < {'a': 1, 'b': 2}aparentemente funciona no Python 2, mas não é confiável porque a documentação oficial o menciona explicitamente. Vá pesquisar "Resultados diferentes da igualdade são resolvidos de forma consistente, mas não são definidos de outra forma." em esta seção . Sem mencionar que a comparação de 2 dictos em Python 3 resulta em uma exceção TypeError.

  2. A segunda coisa mais intuitiva é apenas small.viewitems() <= big.viewitems()para Python 2.7 e small.items() <= big.items()para Python 3. Mas há uma ressalva: é potencialmente bugado . Se o seu programa puder ser usado em Python <= 2.6, ele d1.items() <= d2.items()está, na verdade, comparando 2 listas de tuplas, sem uma ordem específica, então o resultado final não será confiável e se tornará um bug desagradável em seu programa. Não estou interessado em escrever outra implementação para Python <= 2.6, mas ainda não me sinto confortável se meu código vem com um bug conhecido (mesmo se estiver em uma plataforma sem suporte). Portanto, abandono essa abordagem.

  3. Eu me sento com a resposta de @blubberdiblub (o crédito vai para ele):

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

    Vale ressaltar que, essa resposta depende do ==comportamento entre dictos, que está claramente definido em documento oficial, portanto deve funcionar em todas as versões do Python . Vá pesquisar:

    • "Os dicionários são comparados de forma igual se e somente se tiverem os mesmos pares (chave, valor)." é a última frase nesta página
    • "Mapeamentos (instâncias de dict) são comparados iguais se e somente se eles têm pares iguais (chave, valor). A comparação de igualdade das chaves e elementos impõe reflexividade." em esta página
RayLuo
fonte
2

Aqui está uma solução recursiva geral para o problema fornecido:

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: O código original iria falhar em certos casos, os créditos pela correção vão para @olivier-melançon

BPL
fonte
o código falha com um superconjunto que tem um dict aninhado dentro de uma lista, na linhaif not set(value) <= set(superset[key])
Eelco Hoogendoorn
2

Se você não se importa de usar pydash , is_matchexiste um que faz exatamente isso:

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

Eu sei que esta pergunta é antiga, mas aqui está minha solução para verificar se um dicionário aninhado faz parte de outro dicionário aninhado. A solução é recursiva.

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
NutCracker
fonte
0

Esta função funciona para valores não hashable. Também acho que é claro e fácil de ler.

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

Uma implementação recursiva curta que funciona para dicionários aninhados:

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

Isso consumirá os dictos a e b. Se alguém souber de uma boa maneira de evitar isso sem recorrer a soluções parcialmente iterativas como em outras respostas, por favor me diga. Eu precisaria de uma maneira de dividir um dicionário em cabeçalho e cauda com base em uma chave.

Este código é mais útil como um exercício de programação e provavelmente é muito mais lento do que outras soluções aqui que misturam recursão e iteração. A solução do @Nutcracker é muito boa para dicionários aninhados.

Frederik Baetens
fonte
1
Há algo faltando no código. Ele apenas desce o primeiro valor começando em a(e qualquer primeiro valor subsequente) popitemachados. Ele também deve examinar outros itens no mesmo nível. Eu tenho pares de dicts aninhados onde retorna a resposta errada. (difícil apresentar um exemplo à prova de futuro aqui, pois se baseia na ordem de popitem)
blubberdiblub
Obrigado, deve ser corrigido agora :)
Frederik Baetens
0

Use este objeto wrapper que fornece comparação parcial e diferenças legais:


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
fonte