Python: Periksa apakah satu kamus adalah bagian dari kamus lain yang lebih besar

100

Saya mencoba untuk menulis metode filter kustom yang mengambil sejumlah kwarg dan mengembalikan daftar yang berisi elemen dari daftar mirip database yang berisi kwarg tersebut .

Misalnya, d1 = {'a':'2', 'b':'3'}and d2= hal yang sama. d1 == d2menghasilkan True. Tapi misalkan d2= hal yang sama ditambah banyak hal lainnya. Metode saya harus dapat mengetahui apakah d1 dalam d2 , tetapi Python tidak dapat melakukannya dengan kamus.

Konteks:

Aku punya kelas Word, dan setiap objek memiliki sifat seperti word, definition, part_of_speech, dan sebagainya. Saya ingin dapat memanggil metode filter pada daftar utama kata-kata ini, seperti Word.objects.filter(word='jump', part_of_speech='verb-intransitive'). Saya tidak tahu cara mengelola kunci dan nilai ini secara bersamaan. Tetapi ini bisa memiliki fungsi yang lebih besar di luar konteks ini untuk orang lain.

Jamey
sumber

Jawaban:

108

Ubah menjadi pasangan item dan periksa penahanan.

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

Optimasi dibiarkan sebagai latihan bagi pembaca.

Ignacio Vazquez-Abrams
sumber
18
Jika nilai-nilai dict yang hashable, menggunakan viewitems () adalah cara yang paling optimizied saya bisa memikirkan: d1.viewitems() <= d2.viewitems(). Waktu berjalan menunjukkan peningkatan kinerja 3x lipat. Jika tidak dapat dicirikan, bahkan menggunakan iteritems()alih-alih items()mengarah ke peningkatan sekitar 1,2x. Ini dilakukan menggunakan Python 2.7.
Chad
34
saya tidak berpikir pengoptimalan harus diserahkan kepada pembaca - saya khawatir orang akan benar-benar menggunakan ini tanpa menyadarinya akan membuat salinan superset.items () dan mengulanginya untuk setiap item dalam subset.
robert king
4
Dengan Python 3 items()akan mengembalikan tampilan ringan, bukan salinan. Tidak diperlukan pengoptimalan lebih lanjut.
Kentzo
3
Bagaimana dengan direktori bertingkat?
Andreas Profous
5
ini sangat lucu. Saya akan menyerahkan penyempurnaan subjek humor kepada pembaca.
deepelement
95

Di Python 3, Anda bisa menggunakan dict.items()untuk mendapatkan tampilan set-like dari item dict. Anda kemudian dapat menggunakan <=operator untuk menguji apakah satu tampilan adalah "subset" dari yang lain:

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

Di Python 2.7, gunakan dict.viewitems()untuk melakukan hal yang sama:

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

Di Python 2.6 dan di bawahnya Anda akan membutuhkan solusi yang berbeda, seperti menggunakan all():

all(key in d2 and d2[key] == d1[key] for key in d1)
augurar
sumber
1
untuk python3 ini menjadid1.items() <= d2.items()
radu.ciorba
Peringatan: jika program Anda berpotensi digunakan pada Python 2.6 (atau bahkan di bawah), d1.items() <= d2.items()sebenarnya membandingkan 2 daftar tupel, tanpa urutan tertentu, jadi hasil akhirnya mungkin tidak dapat diandalkan. Untuk alasan ini, saya beralih ke jawaban @blubberdiblub.
RayLuo
1
d1.items() <= d2.items()adalah perilaku yang tidak terdefinisi. Ini tidak didokumentasikan dalam dokumen resmi dan, yang paling penting, ini tidak diuji: github.com/python/cpython/blob/… Jadi ini tergantung pada implementasi.
Rodrigo Martins de Oliveira
2
@RodrigoMartins Ini didokumentasikan di sini : "Untuk tampilan set-like, semua operasi yang ditentukan untuk kelas dasar abstrak collections.abc.Settersedia"
augurar
1
@RodrigoMartins Jika Anda khawatir tentang pengelola di masa mendatang, gabungkan operasi dalam fungsi dengan nama yang jelas atau tambahkan komentar kode. Menurunkan standar kode Anda ke tingkat pengembang yang tidak kompeten adalah ide yang buruk.
pelantikan
36

Catatan untuk orang yang membutuhkan ini untuk pengujian unit: ada juga assertDictContainsSubset()metode di TestCasekelas Python .

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

Namun itu sudah usang di 3.2, tidak yakin mengapa, mungkin ada penggantinya.

gitaarik
sumber
29
penasaran, temukan ini di apa yang baru di 3.2 : Metode assertDictContainsSubset () tidak digunakan lagi karena salah diterapkan dengan argumen dalam urutan yang salah. Ini menciptakan ilusi optik yang sulit di-debug di mana pengujian seperti TestCase (). AssertDictContainsSubset ({'a': 1, 'b': 2}, {'a': 1}) akan gagal. (Kontribusi oleh Raymond Hettinger.)
Pedru
2
Tunggu, sisi kiri diharapkan dan sisi kanan adalah aktual ... Bukankah itu gagal? Satu-satunya hal yang salah dengan fungsinya adalah yang mana yang masuk ke tempat mana yang membingungkan?
JamesHutchison
21

untuk kunci dan nilai, gunakan cek: set(d1.items()).issubset(set(d2.items()))

jika Anda hanya perlu memeriksa kunci: set(d1).issubset(set(d2))

kashchey
sumber
11
Ekspresi pertama tidak akan berfungsi jika nilai apa pun di salah satu kamus tidak dapat di-hash.
Pedro Romano
6
Contoh kedua dapat sedikit dipersingkat dengan menghapus set (d2), karena "issubset menerima iterable apapun". docs.python.org/2/library/stdtypes.html#set
trojjer
Ini salah: d1={'a':1,'b':2}; d2={'a':2,'b':1}-> potongan kedua akan kembali True...
Francesco Pasa
1
@FrancescoPasa Potongan kedua mengatakan secara eksplisit: "jika Anda hanya perlu memeriksa kunci". {'a', 'b'}sebenarnya adalah bagian dari {'a', 'b'};)
DylanYoung
19

Untuk kelengkapan, Anda juga bisa melakukan ini:

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

Namun, saya tidak membuat klaim apa pun tentang kecepatan (atau ketiadaan) atau keterbacaan (atau ketiadaan).

blubberdiblub
sumber
Catatan tambahan: Jawaban lain yang menyebutkan small.viewitems() <= big.viewitems()menjanjikan, tetapi dengan satu peringatan: jika program Anda juga dapat digunakan pada Python 2.6 (atau bahkan di bawah), d1.items() <= d2.items()sebenarnya membandingkan 2 daftar tupel, tanpa urutan tertentu, jadi hasil akhirnya mungkin akan tidak dapat diandalkan. Untuk alasan itu, saya beralih ke jawaban @blubberdiblub. Suara positif.
RayLuo
Ini keren, tetapi sepertinya tidak berfungsi dengan kamus bertingkat.
Frederik Baetens
@FrederikBaetens tidak dimaksudkan untuk itu. Juga, saya percaya tidak ada cara yang diterima secara umum bagaimana melakukan itu, karena ada banyak cara untuk melakukannya dan ada beberapa kemungkinan struktur / batasan yang dapat Anda terapkan pada kamus semacam itu. Berikut adalah beberapa pertanyaan yang muncul di benak: Bagaimana Anda menentukan apakah kamus yang lebih dalam harus diturunkan? Bagaimana dengan objek dari tipe yang memiliki dictkelas dasar? Bagaimana jika belum dan masih berperilaku seperti a dict? Bagaimana jika smalldan bigberisi nilai jenis yang berbeda pada kunci yang cocok yang masih berperilaku seperti dict?
blubberdiblub
Itu adalah poin yang valid, tetapi fungsi dasar yang bekerja dengan dicts bersarang polos seharusnya bagus. Saya memposting contoh di sini , tetapi solusi @ NutCracker lebih baik
Frederik Baetens
Tentu, seandainya itu adalah pertanyaan tentang kamus bersarang (dan memiliki persyaratan yang tepat untuk kamus yang diuraikan), saya mungkin telah mengatasinya. Intinya adalah bahwa solusi untuk kamus bersarang tidak memberikan jawaban yang benar ketika Anda ingin mengetahui apakah sebuah dict adalah subdik dari yang lain dengan cara yang datar (yaitu ketika Anda ingin jawaban yang benar-benar tepat Falseketika nilai-nilai dari dicts yang diteruskan berbeda untuk kunci yang cocok). Atau dengan kata lain: Solusi untuk dicts bersarang belum tentu pengganti drop-in tergantung pada kasus penggunaan.
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

konteks:

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

Fungsi saya untuk tujuan yang sama, melakukan ini secara rekursif:

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

Dalam contoh Anda, dictMatch(d1, d2)harus mengembalikan True meskipun d2 memiliki hal lain di dalamnya, ditambah itu berlaku juga untuk tingkat yang lebih rendah:

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

dictMatch(d1, d2)   # True

Catatan: Mungkin ada solusi yang lebih baik yang menghindari if type(pvalue) is dictklausa dan berlaku untuk kasus yang lebih luas (seperti daftar hash dll). Juga rekursi tidak terbatas di sini jadi gunakan dengan resiko Anda sendiri. ;)

Alois Mahdal
sumber
4

Berikut adalah solusi yang juga berulang dengan benar ke dalam daftar dan set yang terdapat dalam kamus. Anda juga dapat menggunakan ini untuk daftar yang berisi penis dll ...

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

Masalah yang tampaknya langsung ini membuat saya menghabiskan beberapa jam dalam penelitian untuk menemukan solusi yang 100% andal, jadi saya mendokumentasikan apa yang saya temukan dalam jawaban ini.

  1. Berbicara "Pythonic-ally", small_dict <= big_dictakan menjadi cara yang paling intuitif, tapi sayang sekali itu tidak akan berhasil . {'a': 1} < {'a': 1, 'b': 2}tampaknya berfungsi dengan Python 2, tetapi tidak dapat diandalkan karena dokumentasi resmi secara eksplisit menyebutkannya. Lakukan pencarian "Hasil selain kesetaraan diselesaikan secara konsisten, tetapi tidak ditentukan sebaliknya." di bagian ini . Belum lagi, membandingkan 2 dicts dengan Python 3 menghasilkan pengecualian TypeError.

  2. Hal kedua yang paling intuitif adalah small.viewitems() <= big.viewitems()untuk Python 2.7 saja, dan small.items() <= big.items()untuk Python 3. Tapi ada satu peringatan: ini berpotensi buggy . Jika program Anda berpotensi digunakan pada Python <= 2.6, d1.items() <= d2.items()ini sebenarnya membandingkan 2 daftar tupel, tanpa urutan tertentu, sehingga hasil akhirnya tidak dapat diandalkan dan menjadi bug yang tidak menyenangkan dalam program Anda. Saya tidak tertarik untuk menulis implementasi lain untuk Python <= 2.6, tetapi saya masih merasa tidak nyaman bahwa kode saya dilengkapi dengan bug yang diketahui (bahkan jika itu pada platform yang tidak didukung). Jadi saya meninggalkan pendekatan ini.

  3. Saya tenang dengan jawaban @blubberdiblub (Penghargaan diberikan kepadanya):

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

    Perlu ditunjukkan bahwa, jawaban ini bergantung pada ==perilaku antar dicts, yang didefinisikan dengan jelas dalam dokumen resmi, oleh karena itu harus berfungsi di setiap versi Python . Pergi cari:

    • "Kamus membandingkan sama jika dan hanya jika mereka memiliki pasangan (kunci, nilai) yang sama." adalah kalimat terakhir di halaman ini
    • "Pemetaan (contoh dikt) dibandingkan jika dan hanya jika mereka memiliki pasangan (kunci, nilai) yang sama. Perbandingan persamaan dari kunci dan elemen memaksakan refleksivitas." di halaman ini
RayLuo
sumber
2

Berikut solusi rekursif umum untuk masalah yang diberikan:

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

CATATAN: Kode asli akan gagal dalam kasus tertentu, kredit untuk perbaikan diberikan ke @ olivier-melançon

BPL
sumber
kode gagal dengan superset yang memiliki dikt yang bersarang di dalam daftar, di barisif not set(value) <= set(superset[key])
Eelco Hoogendoorn
2

Jika Anda tidak keberatan menggunakan pydash ada is_matchada yang tidak tepat:

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

Saya tahu pertanyaan ini sudah tua, tetapi inilah solusi saya untuk memeriksa apakah satu kamus bersarang adalah bagian dari kamus bersarang lainnya. Solusinya rekursif.

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
Alat pemecah buah keras
sumber
0

Fungsi ini berfungsi untuk nilai yang tidak dapat dicirikan. Saya juga menganggapnya jelas dan mudah dibaca.

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

Implementasi rekursif singkat yang berfungsi untuk kamus bersarang:

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

Ini akan memakan penis a dan b. Jika ada yang tahu cara yang baik untuk menghindarinya tanpa menggunakan solusi berulang sebagian seperti dalam jawaban lain, tolong beri tahu saya. Saya akan membutuhkan cara untuk membagi dikt menjadi kepala dan ekor berdasarkan kunci.

Kode ini lebih berguna sebagai latihan pemrograman, dan mungkin jauh lebih lambat daripada solusi lain di sini yang menggabungkan rekursi dan iterasi. Solusi @ Nutcrer cukup bagus untuk kamus bersarang.

Frederik Baetens
sumber
1
Ada sesuatu yang hilang di kode. Itu hanya menurunkan nilai pertama yang dimulai di a(dan nilai pertama berikutnya) yang popitemditemukan. Itu juga harus memeriksa item lain pada tingkat yang sama. Saya punya sepasang penis bersarang yang mengembalikan jawaban yang salah. (sulit untuk menyajikan contoh bukti masa depan di sini, karena bergantung pada urutan popitem)
blubberdiblub
Terima kasih, harus diperbaiki sekarang :)
Frederik Baetens
0

Gunakan objek pembungkus ini yang memberikan perbandingan parsial dan perbedaan yang bagus:


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
sumber