Python: проверьте, является ли один словарь подмножеством другого более крупного словаря

104

Я пытаюсь написать собственный метод фильтрации, который принимает произвольное количество kwargs и возвращает список, содержащий элементы списка, подобного базе данных, которые содержат эти kwargs .

Например, предположим, что d1 = {'a':'2', 'b':'3'}и d2= одно и то же. d1 == d2приводит к True. Но предположим d2= то же самое плюс кучу других вещей. Мой метод должен иметь возможность определять, находится ли d1 в d2 , но Python не может этого сделать со словарями.

Контекст:

У меня есть класс слово, и каждый объект имеет свойства , такие как word, definition, part_of_speechи так далее. Я хочу иметь возможность вызывать метод фильтрации по основному списку этих слов, например Word.objects.filter(word='jump', part_of_speech='verb-intransitive'). Я не могу понять, как управлять этими ключами и значениями одновременно. Но это могло бы иметь более широкую функциональность вне этого контекста для других людей.

Джейми
источник

Ответы:

110

Преобразуйте в пары предметов и проверьте наличие.

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

Оптимизация оставлена ​​читателю в качестве упражнения.

Игнасио Васкес-Абрамс
источник
18
Если значения Dict являются hashable, используя viewitems () является наиболее optimizied способом я могу думать: d1.viewitems() <= d2.viewitems(). Запуск Timeit показал более чем трехкратное улучшение производительности. Если не хэшируемый, даже использование iteritems()вместо items()приводит к увеличению примерно в 1,2 раза. Это было сделано с использованием Python 2.7.
Чад
34
Я не думаю, что оптимизацию следует оставлять читателю - я беспокоюсь, что люди на самом деле будут использовать это, даже не подозревая, что он собирается создать копию superset.items () и перебирать ее для каждого элемента в подмножестве.
Роберт
4
С Python 3 items()будет возвращать облегченные представления вместо копий. Никакой дальнейшей оптимизации не требуется.
Kentzo
3
А как насчет вложенных каталогов?
Андреас Профоус
5
это весело. Я оставлю читателю тему доработки юмора.
deepelement
98

В Python 3 вы можете использовать, dict.items()чтобы получить набор элементов dict. Затем вы можете использовать <=оператор, чтобы проверить, является ли одно представление "подмножеством" другого:

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

В Python 2.7 используйте, dict.viewitems()чтобы сделать то же самое:

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

В Python 2.6 и ниже вам понадобится другое решение, например all():

all(key in d2 and d2[key] == d1[key] for key in d1)
оборота авгурар
источник
1
для python3 это становитсяd1.items() <= d2.items()
radu.ciorba
Предостережение: если ваша программа потенциально может использоваться на Python 2.6 (или даже ниже), d1.items() <= d2.items()они фактически сравнивают 2 списка кортежей без определенного порядка, поэтому конечный результат, вероятно, будет ненадежным. По этой причине я перехожу на ответ @blubberdiblub.
RayLuo
1
d1.items() <= d2.items()неопределенное поведение. Это не задокументировано в официальных документах и, что наиболее важно, не проверено: github.com/python/cpython/blob/… Так что это зависит от реализации.
Родриго Мартинс де Оливейра,
2
@RodrigoMartins Это задокументировано здесь : «Для представлений, подобных множеству collections.abc.Set, доступны все операции, определенные для абстрактного базового класса »
augurar
1
@RodrigoMartins Если вы беспокоитесь о будущих сопровождающих, оберните операцию в четко названную функцию или добавьте комментарий к коду. Снижение стандартов кода до уровня некомпетентных разработчиков - ужасная идея.
augurar
36

Примечание для людей, которым это нужно для модульного тестирования: assertDictContainsSubset()в TestCaseклассе Python также есть метод .

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

Однако он устарел в версии 3.2, не знаю почему, возможно, для него есть замена.

гитаарик
источник
29
было любопытно, обнаружил это в том, что нового в версии 3.2 : метод assertDictContainsSubset () устарел, потому что он был неправильно реализован с аргументами в неправильном порядке. Это создавало трудно поддающиеся отладке оптические иллюзии, когда тесты вроде TestCase (). AssertDictContainsSubset ({'a': 1, 'b': 2}, {'a': 1}) не выполнялись. (
Предоставлено
2
Подождите, левая сторона ожидаема, а правая - актуальна ... Разве это не сработает? Единственное, что не так с функцией, это то, что она идет в какое место сбивает с толку?
Джеймс Хатчисон 01
21

для проверки ключей и значений используйте: set(d1.items()).issubset(set(d2.items()))

если нужно проверить только ключи: set(d1).issubset(set(d2))

кащей
источник
11
Первое выражение не будет работать, если какое-либо значение в любом из словарей не хешируется.
Педро Романо
6
Второй пример можно немного сократить, удалив набор (d2), поскольку «issubset принимает любую итерацию». docs.python.org/2/library/stdtypes.html#set
trojjer
Это неправильно: d1={'a':1,'b':2}; d2={'a':2,'b':1}-> второй фрагмент вернется True...
Франческо
1
@FrancescoPasa Во втором фрагменте прямо сказано: «если вам нужно проверять только ключи». {'a', 'b'}на самом деле является подмножеством {'a', 'b'};)
DylanYoung
20

Для полноты картины также можно сделать так:

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

Однако я не делаю никаких заявлений относительно скорости (или ее отсутствия) или читаемости (или ее отсутствия).

толстяк
источник
Боковое примечание: упоминание других ответов small.viewitems() <= big.viewitems()было многообещающим, но с одним предостережением: если ваша программа также может использоваться на Python 2.6 (или даже ниже), d1.items() <= d2.items()они фактически сравнивают 2 списка кортежей без определенного порядка, поэтому окончательный результат, вероятно, будет ненадежный. По этой причине я переключаюсь на ответ @blubberdiblub. Проголосовали.
RayLuo
Это круто, но, похоже, не работает с вложенными словарями.
Frederik
@FrederikBaetens это не предназначено. Кроме того, я считаю, что нет общепринятого способа, как это сделать, потому что есть несколько способов сделать это, и есть несколько возможных структур / ограничений, которые вы можете наложить на такие словари. Вот несколько вопросов, которые приходят в голову: Как определить, следует ли углубляться в словарь? А как насчет объектов типа, который имеет dictбазовый класс? Что, если этого не произошло и он все еще ведет себя как dict? Что, если smallи bigсодержат значения другого типа в соответствующем ключе, которые по-прежнему ведут себя как dict?
blubberdiblub
Это верные моменты, но простая функция, работающая с простыми вложенными диктовками, должна подойти. Я разместил здесь пример , но решение @ NutCracker лучше
Фредерик
Конечно, если бы это был вопрос о вложенных словарях (и если бы были изложены точные требования к словарям), у меня, возможно, была бы трещина. Дело в том, что решение для вложенных словарей не дает правильного ответа, когда вы хотите знать, является ли dict подтекстом другого в плоской форме (т.е. когда вы хотите, чтобы ответ был строго, Falseкогда значения переданных dicts разные для совпадающих ключей). Другими словами: решение для вложенных dicts не обязательно является заменой в зависимости от варианта использования.
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

контекст:

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

Моя функция для той же цели, делаю это рекурсивно:

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

В вашем примере dictMatch(d1, d2)должен возвращать True, даже если в d2 есть другие вещи, плюс это применимо также к более низким уровням:

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

dictMatch(d1, d2)   # True

Примечания: Может быть даже лучшее решение, которое избегает этого if type(pvalue) is dictпредложения и применяется к еще более широкому кругу случаев (например, спискам хэшей и т. Д.). Также рекурсия здесь не ограничена, поэтому используйте ее на свой страх и риск. ;)

Алоис Махдаль
источник
4

Вот решение, которое также правильно рекурсивно перестраивается в списки и наборы, содержащиеся в словаре. Вы также можете использовать это для списков, содержащих dicts и т. Д.

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

Эта, казалось бы, простая проблема стоит мне пару часов на поиск 100% надежного решения, поэтому я задокументировал то, что нашел в этом ответе.

  1. Говоря «Pythonic-ally», small_dict <= big_dictбыло бы наиболее интуитивно понятным способом, но жаль, что он не сработает . {'a': 1} < {'a': 1, 'b': 2}вроде бы работает в Python 2, но это ненадежно, потому что это явно указано в официальном документе. Перейти к поиску «Результаты, отличные от равенства, решаются последовательно, но иначе не определяются». в этом разделе . Не говоря уже о том, что сравнение двух диктовок в Python 3 приводит к исключению TypeError.

  2. Вторая наиболее интуитивно понятная вещь - только small.viewitems() <= big.viewitems()для Python 2.7 и small.items() <= big.items()для Python 3. Но есть одно предостережение: он потенциально содержит ошибки . Если ваша программа потенциально может быть использована на Python <= 2.6, на d1.items() <= d2.items()самом деле она сравнивает 2 списка кортежей без определенного порядка, поэтому конечный результат будет ненадежным и станет неприятной ошибкой в ​​вашей программе. Я не хочу писать еще одну реализацию для Python <= 2.6, но мне все еще неудобно, что в моем коде есть известная ошибка (даже если она на неподдерживаемой платформе). Поэтому я отказываюсь от этого подхода.

  3. Я согласен с ответом @blubberdiblub (заслуга ему):

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

    Стоит отметить, что этот ответ основан на ==поведении между dicts, которое четко определено в официальном документе и, следовательно, должно работать в каждой версии Python . Искать:

    • «Словари сравнивают одинаково, если и только если они имеют одинаковые пары (ключ, значение)». это последнее предложение на этой странице
    • «Отображения (экземпляры dict) сравниваются как равные тогда и только тогда, когда они имеют равные пары (ключ, значение). Сравнение равенства ключей и элементов обеспечивает рефлексивность». на этой странице
RayLuo
источник
2

Вот общее рекурсивное решение данной проблемы:

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

ПРИМЕЧАНИЕ. Исходный код в некоторых случаях не работает, за исправление следует @ olivier-melançon.

BPL
источник
код не работает с надмножеством, у которого есть словарный запас, вложенный в список, в строкеif not set(value) <= set(superset[key])
Eelco Hoogendoorn
2

Если вы не против использования, pydash есть is_matchто, что делает именно это:

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

Я знаю, что этот вопрос старый, но вот мое решение для проверки, является ли один вложенный словарь частью другого вложенного словаря. Решение рекурсивное.

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
источник
0

Эта функция работает для нехешируемых значений. Я также считаю, что это понятно и легко читается.

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

Краткая рекурсивная реализация, которая работает для вложенных словарей:

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

Это потребует диктовки a и b. Если кто-нибудь знает хороший способ избежать этого, не прибегая к частично итеративным решениям, как в других ответах, скажите мне. Мне понадобится способ разбить диктант на голову и хвост на основе ключа.

Этот код более полезен в качестве упражнения по программированию и, вероятно, намного медленнее, чем другие решения здесь, которые сочетают рекурсию и итерацию. Решение @ Nutcracker неплохо подходит для вложенных словарей.

Фредерик Бетенс
источник
1
В коде чего-то не хватает. Он просто спускается вниз по первому значению, начиная с найденного a(и к любому последующему первому значению) popitem. Следует также изучить другие предметы на том же уровне. У меня есть пары вложенных dicts, где он возвращает неправильный ответ. (трудно представить здесь перспективный пример, поскольку он основан на порядке popitem)
blubberdiblub
Спасибо, сейчас нужно исправить :)
Frederik
0

Используйте этот объект-оболочку, который обеспечивает частичное сравнение и приятные различия:


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
колипто
источник