Python: Перевірте, чи один словник є підмножиною іншого більшого словника


100

Я намагаюся написати власний метод фільтра, який приймає довільну кількість 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'). Я не можу зрозуміти, як керувати цими ключами та значеннями одночасно. Але це може мати більшу функціональність поза цим контекстом для інших людей.

Відповіді:


108

Перетворити в пари предметів і перевірити їх вміст.

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

Оптимізація залишається як вправа для читача.


18
Якщо значення Dict є hashable, використовуючи viewitems () є найбільш optimizied способом я можу думати: d1.viewitems() <= d2.viewitems(). Пробіги Timeit показали покращення продуктивності в 3 рази. Якщо не хеш, навіть використання iteritems()замість цього items()призводить до покращення в 1,2 рази . Це було зроблено за допомогою Python 2.7.
Чад

34
Я не думаю, що оптимізацію слід залишати читачеві - я переживаю, що люди фактично використовуватимуть це, не розуміючи, що збирається створити копію superset.items () і переглядати її для кожного елемента в підмножині.
Роберт Кінг,

4
З Python 3 items()повертає легкі види замість копій. Подальша оптимізація не потрібна.
Kentzo

3
А як щодо вкладених каталогів?
Андреас Профуз

5
це смішно. Я залишу тему уточнення гумору читачеві.
заглиблення

96

У Python 3 ви можете скористатися ним, dict.items()щоб отримати вигляд елементів, що містять дикт, як набір. Потім за допомогою <=оператора можна перевірити, чи є одне представлення «підмножиною» іншого:

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
Зачекайте, лівий бік очікується, а правий - фактичний ... Чи не повинен цей збій? Єдине, що не так у функції, - це те, що йде в яке місце плутає?
JamesHutchison

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

19

Для повноти ви також можете зробити це:

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

Однак я не заявляю жодних претензій щодо швидкості (або її відсутності) чи читабельності (або її відсутності).


Побічна примітка: Інші відповіді, що згадуються, small.viewitems() <= big.viewitems()були багатообіцяючими, але з одним застереженням: якщо ваша програма також може бути використана на Python 2.6 (або навіть нижче), d1.items() <= d2.items()насправді порівнюється 2 списки кортежів без конкретного порядку, тому кінцевий результат, ймовірно, буде не надійний. З цієї причини я переходжу до відповіді @blubberdiblub. Отримано.
RayLuo

Це круто, але, здається, не працює з вкладеними словниками.
Фредерік Баєтенс

@FrederikBaetens це не призначено. Крім того, я вважаю, що не існує загальноприйнятого способу зробити це, тому що існує декілька способів, якими ви могли б це зробити, і є кілька можливих структур / обмежень, які ви могли б накласти на такі словники. Ось декілька питань, які приходять вам на думку: Як визначити, чи слід спускати більш глибокий словник? Що з об’єктами типу, що має dictбазовий клас? Що робити , якщо він не має і все ще веде себе як dict? Що робити, якщо smallі bigмістять значення різного типу у відповідному ключі, який все ще веде себе як dict?
blubberdiblub

Це дійсні пункти, але основна функція, яка працювала з простими вкладеними диктами, повинна бути приємною. Я розмістив приклад тут , але рішення @ NutCracker краще
Фредерік Баєтенс

Впевнений, якби це було питання про вкладені словники (і якщо були визначені точні вимоги до словників), я, можливо, мав би це зламати. Сенс у тому, що рішення для вкладених словників не дає правильної відповіді, коли ви хочете знати, чи то, що dict є судженням іншого плоским способом (тобто коли ви хочете, щоб відповідь суворо була Falseтоді, коли значення переданих диктів різні для відповідності клавіш). Або іншими словами: Рішення для вкладених диктів не обов'язково є заміною, що випадає, залежно від випадку використання.
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

Ось рішення, яке також правильно перетворюється на списки та набори, що містяться у словнику. Ви також можете використовувати це для списків, що містять дикти та ін ...

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. "Пітонічно-союзник", кажучи, small_dict <= big_dictбув би найбільш інтуїтивним способом, але занадто поганим, що це не спрацює . {'a': 1} < {'a': 1, 'b': 2}начебто працює в Python 2, але це не є надійним, оскільки офіційна документація це прямо закликає. Перейти до пошуку "Результати, окрім рівності, вирішуються послідовно, але не визначено іншим чином". у цьому розділі . Не кажучи вже про те, порівняння 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

    Варто зазначити, що ця відповідь спирається на ==поведінку між диктами, яка чітко визначена в офіційному документі, отже, вона повинна працювати у кожній версії Python . Перейти до пошуку:

    • "Словники порівнюють рівні, якщо і тільки якщо вони мають однакові (ключ, значення) пари." - останнє речення на цій сторінці
    • "Маппінг (екземпляри dict) порівнюють рівні тоді і лише тоді, коли вони мають рівні пари (ключ, значення). Порівняння рівності ключів та елементів забезпечує рефлексивність." на цій сторінці

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


коду не вдається із надмножиною, що має дикт, вкладений всередину списку, у рядок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

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

Це поглине а-b-дикти. Якщо хтось знає хороший спосіб уникнути цього, не вдаючись до частково ітеративних рішень, як у інших відповідях, будь ласка, скажіть мені. Мені знадобився би спосіб розділити дикт на голову та хвіст на основі ключа.

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


1
У коді щось не вистачає. Він просто спускається вниз по першому значенню, починаючи з a(і будь-якого наступного першого значення) popitemзнахідки. Він також повинен вивчити інші предмети на тому ж рівні. У мене є пари вкладених диктів, де він повертає неправильну відповідь. (тут важко представити приклад, що підтверджує майбутнє, оскільки він покладається на порядок popitem)
blubberdiblub

Дякую, слід виправити зараз :)
Фредерік Баєтенс

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
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.