Apa cara terbaik untuk mengimplementasikan kamus bersarang?


201

Saya memiliki struktur data yang pada dasarnya berjumlah kamus bersarang. Katakanlah ini terlihat seperti ini:

{'new jersey': {'mercer county': {'plumbers': 3,
                                  'programmers': 81},
                'middlesex county': {'programmers': 81,
                                     'salesmen': 62}},
 'new york': {'queens county': {'plumbers': 9,
                                'salesmen': 36}}}

Sekarang, memelihara dan menciptakan ini sangat menyakitkan; setiap kali saya memiliki negara bagian / daerah / profesi baru saya harus membuat kamus lapisan bawah melalui blok coba / tangkap yang menjengkelkan. Selain itu, saya harus membuat iterator bersarang menjengkelkan jika saya ingin membahas semua nilai.

Saya juga bisa menggunakan tuple sebagai kunci, seperti:

{('new jersey', 'mercer county', 'plumbers'): 3,
 ('new jersey', 'mercer county', 'programmers'): 81,
 ('new jersey', 'middlesex county', 'programmers'): 81,
 ('new jersey', 'middlesex county', 'salesmen'): 62,
 ('new york', 'queens county', 'plumbers'): 9,
 ('new york', 'queens county', 'salesmen'): 36}

Hal ini membuat pengulangan nilai-nilai menjadi sangat sederhana dan alami, tetapi secara sintaksis menyakitkan untuk melakukan hal-hal seperti agregasi dan melihat himpunan bagian dari kamus (misalnya jika saya hanya ingin pergi negara-oleh-negara).

Pada dasarnya, kadang-kadang saya ingin menganggap kamus bersarang sebagai kamus datar, dan terkadang saya ingin menganggapnya sebagai hierarki yang kompleks. Saya bisa membungkus ini semua dalam satu kelas, tetapi sepertinya seseorang mungkin sudah melakukan ini. Atau, sepertinya ada beberapa konstruksi sintaksis yang sangat elegan untuk melakukan ini.

Bagaimana saya bisa melakukan ini dengan lebih baik?

Tambahan: Saya sadar setdefault()tetapi itu tidak benar-benar membuat sintaksis bersih. Juga, setiap sub-kamus yang Anda buat masih harus setdefault()diatur secara manual.

Jawaban:


179

Apa cara terbaik untuk mengimplementasikan kamus bersarang di Python?

Ini ide yang buruk, jangan lakukan itu. Sebagai gantinya, gunakan kamus reguler dan gunakan di dict.setdefaultmana yang sesuai, jadi ketika kunci hilang dalam penggunaan normal Anda mendapatkan yang diharapkanKeyError . Jika Anda bersikeras untuk mendapatkan perilaku ini, berikut cara menembak diri sendiri:

Terapkan __missing__pada adict subclass untuk mengatur dan mengembalikan instance baru.

Pendekatan ini telah tersedia (dan didokumentasikan) sejak Python 2.5, dan (terutama berharga bagi saya) itu cukup mencetak seperti dict normal , alih-alih pencetakan jelek dari defaultdict autovivified otomatis:

class Vividict(dict):
    def __missing__(self, key):
        value = self[key] = type(self)() # retain local pointer to value
        return value                     # faster to return than dict lookup

(Catatan self[key]ada di sisi kiri penugasan, jadi tidak ada rekursi di sini.)

dan katakan Anda memiliki beberapa data:

data = {('new jersey', 'mercer county', 'plumbers'): 3,
        ('new jersey', 'mercer county', 'programmers'): 81,
        ('new jersey', 'middlesex county', 'programmers'): 81,
        ('new jersey', 'middlesex county', 'salesmen'): 62,
        ('new york', 'queens county', 'plumbers'): 9,
        ('new york', 'queens county', 'salesmen'): 36}

Inilah kode penggunaan kami:

vividict = Vividict()
for (state, county, occupation), number in data.items():
    vividict[state][county][occupation] = number

Dan sekarang:

>>> import pprint
>>> pprint.pprint(vividict, width=40)
{'new jersey': {'mercer county': {'plumbers': 3,
                                  'programmers': 81},
                'middlesex county': {'programmers': 81,
                                     'salesmen': 62}},
 'new york': {'queens county': {'plumbers': 9,
                                'salesmen': 36}}}

Kritik

Kritik terhadap jenis wadah ini adalah jika pengguna salah mengeja kunci, kode kami bisa gagal secara diam-diam:

>>> vividict['new york']['queens counyt']
{}

Dan juga sekarang kita akan memiliki county yang salah eja dalam data kami:

>>> pprint.pprint(vividict, width=40)
{'new jersey': {'mercer county': {'plumbers': 3,
                                  'programmers': 81},
                'middlesex county': {'programmers': 81,
                                     'salesmen': 62}},
 'new york': {'queens county': {'plumbers': 9,
                                'salesmen': 36},
              'queens counyt': {}}}

Penjelasan:

Kami hanya menyediakan contoh lain dari kelas kami Vividict setiap kali kunci diakses tetapi tidak ada. (Mengembalikan penugasan nilai berguna karena ia menghindari kami juga memanggil pengambil pada dikt, dan sayangnya, kami tidak dapat mengembalikannya ketika sedang ditetapkan.)

Catatan, ini adalah semantik yang sama dengan jawaban yang paling banyak dipilih tetapi dalam setengah baris kode - implementasi nosklo:

class AutoVivification(dict):
    """Implementation of perl's autovivification feature."""
    def __getitem__(self, item):
        try:
            return dict.__getitem__(self, item)
        except KeyError:
            value = self[item] = type(self)()
            return value

Demonstrasi Penggunaan

Di bawah ini adalah contoh bagaimana dict ini dapat dengan mudah digunakan untuk membuat struktur dict bersarang dengan cepat. Ini dapat dengan cepat membuat struktur pohon hierarkis sedalam yang Anda inginkan.

import pprint

class Vividict(dict):
    def __missing__(self, key):
        value = self[key] = type(self)()
        return value

d = Vividict()

d['foo']['bar']
d['foo']['baz']
d['fizz']['buzz']
d['primary']['secondary']['tertiary']['quaternary']
pprint.pprint(d)

Output yang mana:

{'fizz': {'buzz': {}},
 'foo': {'bar': {}, 'baz': {}},
 'primary': {'secondary': {'tertiary': {'quaternary': {}}}}}

Dan seperti yang ditunjukkan baris terakhir, itu cukup mencetak dengan indah dan untuk inspeksi manual. Tetapi jika Anda ingin secara visual memeriksa data Anda, menerapkan __missing__untuk menetapkan contoh baru dari kelasnya ke kunci dan mengembalikannya adalah solusi yang jauh lebih baik.

Alternatif lain, untuk kontras:

dict.setdefault

Meskipun penanya berpikir ini tidak bersih, saya merasa lebih baik daripada Vividictsaya sendiri.

d = {} # or dict()
for (state, county, occupation), number in data.items():
    d.setdefault(state, {}).setdefault(county, {})[occupation] = number

dan sekarang:

>>> pprint.pprint(d, width=40)
{'new jersey': {'mercer county': {'plumbers': 3,
                                  'programmers': 81},
                'middlesex county': {'programmers': 81,
                                     'salesmen': 62}},
 'new york': {'queens county': {'plumbers': 9,
                                'salesmen': 36}}}

Salah mengeja akan gagal dengan ribut, dan tidak mengacaukan data kami dengan informasi yang buruk:

>>> d['new york']['queens counyt']
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
KeyError: 'queens counyt'

Selain itu, saya pikir setdefault berfungsi dengan baik ketika digunakan dalam loop dan Anda tidak tahu apa yang akan Anda dapatkan untuk kunci, tetapi penggunaan berulang menjadi cukup memberatkan, dan saya tidak berpikir ada orang yang ingin mengikuti yang berikut:

d = dict()

d.setdefault('foo', {}).setdefault('bar', {})
d.setdefault('foo', {}).setdefault('baz', {})
d.setdefault('fizz', {}).setdefault('buzz', {})
d.setdefault('primary', {}).setdefault('secondary', {}).setdefault('tertiary', {}).setdefault('quaternary', {})

Kritik lain adalah bahwa setdefault membutuhkan contoh baru apakah itu digunakan atau tidak. Namun, Python (atau setidaknya CPython) agak pintar menangani kasus baru yang tidak digunakan dan tidak direferensikan, misalnya, menggunakan kembali lokasi dalam memori:

>>> id({}), id({}), id({})
(523575344, 523575344, 523575344)

Sebuah defaultdict vivified otomatis

Ini adalah implementasi yang tampak rapi, dan penggunaan dalam skrip yang tidak Anda periksa datanya akan sama bermanfaatnya dengan penerapan __missing__:

from collections import defaultdict

def vivdict():
    return defaultdict(vivdict)

Tetapi jika Anda perlu memeriksa data Anda, hasil dari default-vivified defaultdict diisi dengan data dengan cara yang sama terlihat seperti ini:

>>> d = vivdict(); d['foo']['bar']; d['foo']['baz']; d['fizz']['buzz']; d['primary']['secondary']['tertiary']['quaternary']; import pprint; 
>>> pprint.pprint(d)
defaultdict(<function vivdict at 0x17B01870>, {'foo': defaultdict(<function vivdict 
at 0x17B01870>, {'baz': defaultdict(<function vivdict at 0x17B01870>, {}), 'bar': 
defaultdict(<function vivdict at 0x17B01870>, {})}), 'primary': defaultdict(<function 
vivdict at 0x17B01870>, {'secondary': defaultdict(<function vivdict at 0x17B01870>, 
{'tertiary': defaultdict(<function vivdict at 0x17B01870>, {'quaternary': defaultdict(
<function vivdict at 0x17B01870>, {})})})}), 'fizz': defaultdict(<function vivdict at 
0x17B01870>, {'buzz': defaultdict(<function vivdict at 0x17B01870>, {})})})

Output ini cukup tidak elegan, dan hasilnya cukup tidak dapat dibaca. Solusi yang biasanya diberikan adalah mengkonversi secara rekursif ke dikt untuk inspeksi manual. Solusi non-sepele ini dibiarkan sebagai latihan bagi pembaca.

Performa

Akhirnya, mari kita lihat kinerja. Saya mengurangi biaya instantiation.

>>> import timeit
>>> min(timeit.repeat(lambda: {}.setdefault('foo', {}))) - min(timeit.repeat(lambda: {}))
0.13612580299377441
>>> min(timeit.repeat(lambda: vivdict()['foo'])) - min(timeit.repeat(lambda: vivdict()))
0.2936999797821045
>>> min(timeit.repeat(lambda: Vividict()['foo'])) - min(timeit.repeat(lambda: Vividict()))
0.5354437828063965
>>> min(timeit.repeat(lambda: AutoVivification()['foo'])) - min(timeit.repeat(lambda: AutoVivification()))
2.138362169265747

Berdasarkan kinerja, dict.setdefaultbekerja yang terbaik. Saya sangat merekomendasikannya untuk kode produksi, jika Anda peduli dengan kecepatan eksekusi.

Jika Anda memerlukan ini untuk penggunaan interaktif (dalam notebook IPython, mungkin) maka kinerja tidak terlalu penting - dalam hal ini, saya akan menggunakan Vividict untuk keterbacaan output. Dibandingkan dengan objek AutoVivification (yang menggunakan __getitem__alih-alih __missing__, yang dibuat untuk tujuan ini) jauh lebih unggul.

Kesimpulan

Menerapkan __missing__pada subclass dictuntuk mengatur dan mengembalikan contoh baru sedikit lebih sulit daripada alternatif tetapi memiliki manfaat

  • Instansiasi mudah
  • populasi data mudah
  • tampilan data mudah

dan karena kurang rumit dan lebih berkinerja daripada memodifikasi __getitem__, itu harus lebih disukai daripada metode itu.

Namun demikian, ia memiliki kekurangan:

  • Pencarian buruk akan gagal secara diam-diam.
  • Pencarian buruk akan tetap ada di kamus.

Jadi saya pribadi lebih suka setdefaultsolusi lain, dan ada dalam setiap situasi di mana saya membutuhkan perilaku semacam ini.


Jawaban yang sangat bagus! Apakah ada cara untuk menentukan kedalaman terbatas dan tipe daun untuk a Vividict? Misalnya 3dan listuntuk dict dari dict dari daftar yang dapat diisi dengan d['primary']['secondary']['tertiary'].append(element). Saya dapat mendefinisikan 3 kelas berbeda untuk setiap kedalaman tetapi saya ingin menemukan solusi yang lebih bersih.
Eric Duminil

@EricDuminil d['primary']['secondary'].setdefault('tertiary', []).append('element')- ?? Terima kasih atas pujiannya, tetapi biarkan saya jujur ​​- saya tidak pernah benar-benar menggunakan __missing__- saya selalu menggunakan setdefault. Saya mungkin harus memperbarui kesimpulan / intro saya ...
Aaron Hall

@ AaronHall Perilaku yang benar adalah kode harus membuat dict jika diperlukan. Dalam hal ini dengan menimpa nilai yang ditugaskan sebelumnya.
nehem

@AaronHall Anda juga dapat membantu saya memahami apa yang dimaksud dengan The bad lookup will remain in the dictionary.saya mempertimbangkan untuk menggunakan solusi ini? Sangat dihargai. Thx
nehem

@ AaronHall Masalah dengan itu akan gagal setdefaultketika bersarang lebih dari dua level. Sepertinya tidak ada struktur di Python yang dapat menawarkan vivifikasi sejati seperti yang dijelaskan. Saya harus puas dengan dua metode menyatakan satu untuk get_nested& satu set_nestedyang menerima referensi untuk dict dan daftar atribut bersarang.
nehem

188
class AutoVivification(dict):
    """Implementation of perl's autovivification feature."""
    def __getitem__(self, item):
        try:
            return dict.__getitem__(self, item)
        except KeyError:
            value = self[item] = type(self)()
            return value

Pengujian:

a = AutoVivification()

a[1][2][3] = 4
a[1][3][3] = 5
a[1][2]['test'] = 6

print a

Keluaran:

{1: {2: {'test': 6, 3: 4}, 3: {3: 5}}}

Adakah yang memiliki masalah ini ketika mereka pindah ke python 3.x? stackoverflow.com/questions/54622935/…
jason

@ alasannya picklemengerikan antara versi python. Hindari menggunakannya untuk menyimpan data yang ingin Anda simpan. Gunakan hanya untuk cache dan barang-barang yang bisa Anda buang dan buat ulang sesuka hati. Bukan sebagai metode penyimpanan atau serialisasi jangka panjang.
nosklo

Apa yang Anda gunakan untuk menyimpan benda-benda ini? Objek autovivifikasi saya hanya berisi kerangka data dan string panda.
jason

@ alasan Tergantung pada data, saya suka menggunakan JSON, file csv, atau bahkan sqlitedatabase untuk menyimpannya.
nosklo

30

Hanya karena saya belum melihat yang sekecil ini, inilah dict yang bersarang sebanyak yang Anda inginkan, tanpa keringat:

# yo dawg, i heard you liked dicts                                                                      
def yodict():
    return defaultdict(yodict)

2
@wberry: Sebenarnya yang Anda butuhkan adalah yodict = lambda: defaultdict(yodict).
martineau

1
Versi yang diterima adalah subkelas dari dict, jadi untuk menjadi sepenuhnya setara kita perlu x = Vdict(a=1, b=2)bekerja.
wberry

@wberry: Terlepas dari apa yang ada di jawaban yang diterima, menjadi subclass dari dictbukan persyaratan yang dinyatakan oleh OP, yang hanya meminta "cara terbaik" untuk mengimplementasikannya - dan selain itu, itu tidak / tidak seharusnya masalah sebanyak itu di Python.
martineau

24

Anda bisa membuat file YAML dan membacanya menggunakan PyYaml .

Langkah 1: Buat file YAML, "employment.yml":

new jersey:
  mercer county:
    pumbers: 3
    programmers: 81
  middlesex county:
    salesmen: 62
    programmers: 81
new york:
  queens county:
    plumbers: 9
    salesmen: 36

Langkah 2: Baca dengan Python

import yaml
file_handle = open("employment.yml")
my_shnazzy_dictionary = yaml.safe_load(file_handle)
file_handle.close()

dan sekarang my_shnazzy_dictionarymemiliki semua nilai Anda. Jika Anda perlu melakukan ini dengan cepat, Anda dapat membuat YAML sebagai string dan memasukkannya ke dalam yaml.safe_load(...).


4
YAML jelas merupakan pilihan saya untuk memasukkan banyak data yang sangat bersarang (dan file konfigurasi, maket data, dll ...). Jika OP tidak ingin file tambahan tergeletak, cukup gunakan string Python biasa di beberapa file dan parsing dengan YAML.
kmelvn

Poin bagus dalam membuat string YAML: Ini akan menjadi pendekatan yang jauh lebih bersih daripada menggunakan modul "tempfile" berulang kali.
Pete

18

Karena Anda memiliki desain skema bintang, Anda mungkin ingin menyusunnya lebih seperti tabel relasional dan kurang seperti kamus.

import collections

class Jobs( object ):
    def __init__( self, state, county, title, count ):
        self.state= state
        self.count= county
        self.title= title
        self.count= count

facts = [
    Jobs( 'new jersey', 'mercer county', 'plumbers', 3 ),
    ...

def groupBy( facts, name ):
    total= collections.defaultdict( int )
    for f in facts:
        key= getattr( f, name )
        total[key] += f.count

Hal semacam itu bisa sangat berarti untuk menciptakan desain seperti data warehouse tanpa overhead SQL.


14

Jika jumlah level bersarang kecil, saya gunakan collections.defaultdictuntuk ini:

from collections import defaultdict

def nested_dict_factory(): 
  return defaultdict(int)
def nested_dict_factory2(): 
  return defaultdict(nested_dict_factory)
db = defaultdict(nested_dict_factory2)

db['new jersey']['mercer county']['plumbers'] = 3
db['new jersey']['mercer county']['programmers'] = 81

Menggunakan defaultdictseperti ini menghindari banyak berantakan setdefault(), get()dll


+1: defaultdict adalah salah satu tambahan favorit saya sepanjang masa untuk python. Tidak ada lagi .setdefault ()!
John Fouhy

8

Ini adalah fungsi yang mengembalikan kamus bersarang dari kedalaman arbitrer:

from collections import defaultdict
def make_dict():
    return defaultdict(make_dict)

Gunakan seperti ini:

d=defaultdict(make_dict)
d["food"]["meat"]="beef"
d["food"]["veggie"]="corn"
d["food"]["sweets"]="ice cream"
d["animal"]["pet"]["dog"]="collie"
d["animal"]["pet"]["cat"]="tabby"
d["animal"]["farm animal"]="chicken"

Ulangi semuanya dengan sesuatu seperti ini:

def iter_all(d,depth=1):
    for k,v in d.iteritems():
        print "-"*depth,k
        if type(v) is defaultdict:
            iter_all(v,depth+1)
        else:
            print "-"*(depth+1),v

iter_all(d)

Ini mencetak:

- food
-- sweets
--- ice cream
-- meat
--- beef
-- veggie
--- corn
- animal
-- pet
--- dog
---- labrador
--- cat
---- tabby
-- farm animal
--- chicken

Anda mungkin pada akhirnya ingin membuatnya sehingga item baru tidak dapat ditambahkan ke dikt. Sangat mudah untuk secara rekursif mengubah semua ini defaultdictmenjadi normal dict.

def dictify(d):
    for k,v in d.iteritems():
        if isinstance(v,defaultdict):
            d[k] = dictify(v)
    return dict(d)

7

Saya menemukan setdefaultcukup berguna; Ia memeriksa apakah ada kunci dan menambahkannya jika tidak:

d = {}
d.setdefault('new jersey', {}).setdefault('mercer county', {})['plumbers'] = 3

setdefault selalu mengembalikan kunci yang relevan, sehingga Anda benar-benar memperbarui nilai 'd ' di tempat.

Ketika datang ke iterasi, saya yakin Anda bisa menulis generator dengan cukup mudah jika belum ada di Python:

def iterateStates(d):
    # Let's count up the total number of "plumbers" / "dentists" / etc.
    # across all counties and states
    job_totals = {}

    # I guess this is the annoying nested stuff you were talking about?
    for (state, counties) in d.iteritems():
        for (county, jobs) in counties.iteritems():
            for (job, num) in jobs.iteritems():
                # If job isn't already in job_totals, default it to zero
                job_totals[job] = job_totals.get(job, 0) + num

    # Now return an iterator of (job, number) tuples
    return job_totals.iteritems()

# Display all jobs
for (job, num) in iterateStates(d):
    print "There are %d %s in total" % (job, num)

Saya suka solusi ini tetapi ketika saya mencoba: count.setdefault (a, {}). Setdefault (b, {}). Setdefault (c, 0) + = 1 Saya mendapatkan "ekspresi ilegal untuk penugasan yang diperbesar"
dfrankow

6

Seperti yang disarankan orang lain, basis data relasional bisa lebih bermanfaat bagi Anda. Anda bisa menggunakan database sqlite3 di memori sebagai struktur data untuk membuat tabel dan kemudian meminta mereka.

import sqlite3

c = sqlite3.Connection(':memory:')
c.execute('CREATE TABLE jobs (state, county, title, count)')

c.executemany('insert into jobs values (?, ?, ?, ?)', [
    ('New Jersey', 'Mercer County',    'Programmers', 81),
    ('New Jersey', 'Mercer County',    'Plumbers',     3),
    ('New Jersey', 'Middlesex County', 'Programmers', 81),
    ('New Jersey', 'Middlesex County', 'Salesmen',    62),
    ('New York',   'Queens County',    'Salesmen',    36),
    ('New York',   'Queens County',    'Plumbers',     9),
])

# some example queries
print list(c.execute('SELECT * FROM jobs WHERE county = "Queens County"'))
print list(c.execute('SELECT SUM(count) FROM jobs WHERE title = "Programmers"'))

Ini hanyalah contoh sederhana. Anda bisa menentukan tabel terpisah untuk negara, kabupaten, dan jabatan.


5

collections.defaultdictdapat di-sub-class untuk membuat dict bersarang. Kemudian tambahkan metode iterasi yang berguna ke kelas itu.

>>> from collections import defaultdict
>>> class nesteddict(defaultdict):
    def __init__(self):
        defaultdict.__init__(self, nesteddict)
    def walk(self):
        for key, value in self.iteritems():
            if isinstance(value, nesteddict):
                for tup in value.walk():
                    yield (key,) + tup
            else:
                yield key, value


>>> nd = nesteddict()
>>> nd['new jersey']['mercer county']['plumbers'] = 3
>>> nd['new jersey']['mercer county']['programmers'] = 81
>>> nd['new jersey']['middlesex county']['programmers'] = 81
>>> nd['new jersey']['middlesex county']['salesmen'] = 62
>>> nd['new york']['queens county']['plumbers'] = 9
>>> nd['new york']['queens county']['salesmen'] = 36
>>> for tup in nd.walk():
    print tup


('new jersey', 'mercer county', 'programmers', 81)
('new jersey', 'mercer county', 'plumbers', 3)
('new jersey', 'middlesex county', 'programmers', 81)
('new jersey', 'middlesex county', 'salesmen', 62)
('new york', 'queens county', 'salesmen', 36)
('new york', 'queens county', 'plumbers', 9)

1
Ini adalah jawaban yang paling dekat dengan apa yang saya cari. Tetapi idealnya akan ada segala macam fungsi pembantu, misalnya walk_keys () atau semacamnya. Saya terkejut tidak ada di perpustakaan standar untuk melakukan ini.
YGA

4

Adapun "blok coba / tangkap yang menjengkelkan":

d = {}
d.setdefault('key',{}).setdefault('inner key',{})['inner inner key'] = 'value'
print d

hasil panen

{'key': {'inner key': {'inner inner key': 'value'}}}

Anda dapat menggunakan ini untuk mengonversi dari format kamus Anda ke format terstruktur:

fd = {('new jersey', 'mercer county', 'plumbers'): 3,
 ('new jersey', 'mercer county', 'programmers'): 81,
 ('new jersey', 'middlesex county', 'programmers'): 81,
 ('new jersey', 'middlesex county', 'salesmen'): 62,
 ('new york', 'queens county', 'plumbers'): 9,
 ('new york', 'queens county', 'salesmen'): 36}

for (k1,k2,k3), v in fd.iteritems():
    d.setdefault(k1, {}).setdefault(k2, {})[k3] = v

4

Anda dapat menggunakan Addict: https://github.com/mewwts/addict

>>> from addict import Dict
>>> my_new_shiny_dict = Dict()
>>> my_new_shiny_dict.a.b.c.d.e = 2
>>> my_new_shiny_dict
{'a': {'b': {'c': {'d': {'e': 2}}}}}

4

defaultdict() adalah temanmu!

Untuk kamus dua dimensi yang dapat Anda lakukan:

d = defaultdict(defaultdict)
d[1][2] = 3

Untuk lebih banyak dimensi Anda dapat:

d = defaultdict(lambda :defaultdict(defaultdict))
d[1][2][3] = 4

Jawaban ini hanya berfungsi untuk tiga level terbaik. Untuk level sewenang-wenang, pertimbangkan jawaban ini .
Acumenus

3

Untuk memudahkan pengulangan pada kamus bersarang Anda, mengapa tidak menulis generator sederhana?

def each_job(my_dict):
    for state, a in my_dict.items():
        for county, b in a.items():
            for job, value in b.items():
                yield {
                    'state'  : state,
                    'county' : county,
                    'job'    : job,
                    'value'  : value
                }

Jadi, jika Anda memiliki kamus bersarang yang dikompilasi, iterasi menjadi sederhana:

for r in each_job(my_dict):
    print "There are %d %s in %s, %s" % (r['value'], r['job'], r['county'], r['state'])

Jelas generator Anda dapat menghasilkan format data apa pun yang berguna bagi Anda.

Mengapa Anda menggunakan coba tangkap balok untuk membaca pohon? Cukup mudah (dan mungkin lebih aman) untuk menanyakan apakah ada kunci dalam dict sebelum mencoba mengambilnya. Fungsi yang menggunakan klausa penjaga mungkin terlihat seperti ini:

if not my_dict.has_key('new jersey'):
    return False

nj_dict = my_dict['new jersey']
...

Atau, metode yang mungkin agak bertele-tele, adalah menggunakan metode get:

value = my_dict.get('new jersey', {}).get('middlesex county', {}).get('salesmen', 0)

Tetapi untuk cara yang agak lebih ringkas, Anda mungkin ingin melihat menggunakan collections.defaultdict , yang merupakan bagian dari pustaka standar sejak python 2.5.

import collections

def state_struct(): return collections.defaultdict(county_struct)
def county_struct(): return collections.defaultdict(job_struct)
def job_struct(): return 0

my_dict = collections.defaultdict(state_struct)

print my_dict['new jersey']['middlesex county']['salesmen']

Saya membuat asumsi tentang arti struktur data Anda di sini, tetapi seharusnya mudah untuk menyesuaikan dengan apa yang sebenarnya ingin Anda lakukan.


2

Saya suka ide membungkus ini di kelas dan mengimplementasikan __getitem__dan __setitem__sedemikian rupa sehingga mereka menerapkan bahasa permintaan sederhana:

>>> d['new jersey/mercer county/plumbers'] = 3
>>> d['new jersey/mercer county/programmers'] = 81
>>> d['new jersey/mercer county/programmers']
81
>>> d['new jersey/mercer country']
<view which implicitly adds 'new jersey/mercer county' to queries/mutations>

Jika Anda ingin menjadi mewah, Anda juga bisa menerapkan sesuatu seperti:

>>> d['*/*/programmers']
<view which would contain 'programmers' entries>

tapi kebanyakan saya pikir hal seperti itu akan sangat menyenangkan untuk diterapkan: D


Saya pikir ini adalah ide yang buruk - Anda tidak pernah dapat memprediksi sintaksis kunci. Anda masih akan mengganti getitem dan setitem tetapi minta mereka mengambil tupel.
YGA

3
@YGA Anda mungkin benar, tetapi menyenangkan untuk berpikir tentang menerapkan bahasa mini seperti ini.
Aaron Maenpaa

1

Kecuali jika dataset Anda akan tetap sangat kecil, Anda mungkin ingin mempertimbangkan untuk menggunakan basis data relasional. Ini akan melakukan apa yang Anda inginkan: membuatnya mudah untuk menambah jumlah, memilih himpunan bagian dari jumlah, dan bahkan jumlah agregat berdasarkan negara, wilayah, pekerjaan, atau kombinasi dari semua ini.


1
class JobDb(object):
    def __init__(self):
        self.data = []
        self.all = set()
        self.free = []
        self.index1 = {}
        self.index2 = {}
        self.index3 = {}

    def _indices(self,(key1,key2,key3)):
        indices = self.all.copy()
        wild = False
        for index,key in ((self.index1,key1),(self.index2,key2),
                                             (self.index3,key3)):
            if key is not None:
                indices &= index.setdefault(key,set())
            else:
                wild = True
        return indices, wild

    def __getitem__(self,key):
        indices, wild = self._indices(key)
        if wild:
            return dict(self.data[i] for i in indices)
        else:
            values = [self.data[i][-1] for i in indices]
            if values:
                return values[0]

    def __setitem__(self,key,value):
        indices, wild = self._indices(key)
        if indices:
            for i in indices:
                self.data[i] = key,value
        elif wild:
            raise KeyError(k)
        else:
            if self.free:
                index = self.free.pop(0)
                self.data[index] = key,value
            else:
                index = len(self.data)
                self.data.append((key,value))
                self.all.add(index)
            self.index1.setdefault(key[0],set()).add(index)
            self.index2.setdefault(key[1],set()).add(index)
            self.index3.setdefault(key[2],set()).add(index)

    def __delitem__(self,key):
        indices,wild = self._indices(key)
        if not indices:
            raise KeyError
        self.index1[key[0]] -= indices
        self.index2[key[1]] -= indices
        self.index3[key[2]] -= indices
        self.all -= indices
        for i in indices:
            self.data[i] = None
        self.free.extend(indices)

    def __len__(self):
        return len(self.all)

    def __iter__(self):
        for key,value in self.data:
            yield key

Contoh:

>>> db = JobDb()
>>> db['new jersey', 'mercer county', 'plumbers'] = 3
>>> db['new jersey', 'mercer county', 'programmers'] = 81
>>> db['new jersey', 'middlesex county', 'programmers'] = 81
>>> db['new jersey', 'middlesex county', 'salesmen'] = 62
>>> db['new york', 'queens county', 'plumbers'] = 9
>>> db['new york', 'queens county', 'salesmen'] = 36

>>> db['new york', None, None]
{('new york', 'queens county', 'plumbers'): 9,
 ('new york', 'queens county', 'salesmen'): 36}

>>> db[None, None, 'plumbers']
{('new jersey', 'mercer county', 'plumbers'): 3,
 ('new york', 'queens county', 'plumbers'): 9}

>>> db['new jersey', 'mercer county', None]
{('new jersey', 'mercer county', 'plumbers'): 3,
 ('new jersey', 'mercer county', 'programmers'): 81}

>>> db['new jersey', 'middlesex county', 'programmers']
81

>>>

Sunting: Sekarang kembali kamus ketika kueri dengan wild card ( None), dan nilai-nilai tunggal sebaliknya.


Mengapa mengembalikan daftar? Tampaknya itu harus mengembalikan kamus (sehingga Anda tahu apa yang mewakili angka masing-masing) atau jumlah (karena hanya itu yang dapat Anda lakukan dengan daftar).
Ben Blank

0

Saya memiliki hal serupa terjadi. Saya memiliki banyak kasus di mana saya melakukannya:

thedict = {}
for item in ('foo', 'bar', 'baz'):
  mydict = thedict.get(item, {})
  mydict = get_value_for(item)
  thedict[item] = mydict

Tetapi pergi ke banyak level. Ini ".get (item, {})" itulah kuncinya karena akan membuat kamus lain jika belum ada. Sementara itu, saya sudah memikirkan cara untuk menghadapi ini dengan lebih baik. Saat ini, ada banyak

value = mydict.get('foo', {}).get('bar', {}).get('baz', 0)

Jadi sebagai gantinya, saya membuat:

def dictgetter(thedict, default, *args):
  totalargs = len(args)
  for i,arg in enumerate(args):
    if i+1 == totalargs:
      thedict = thedict.get(arg, default)
    else:
      thedict = thedict.get(arg, {})
  return thedict

Yang memiliki efek yang sama jika Anda melakukannya:

value = dictgetter(mydict, 0, 'foo', 'bar', 'baz')

Lebih baik? Aku pikir begitu.


0

Anda dapat menggunakan rekursi dalam lambdas dan defaultdict, tidak perlu mendefinisikan nama:

a = defaultdict((lambda f: f(f))(lambda g: lambda:defaultdict(g(g))))

Ini sebuah contoh:

>>> a['new jersey']['mercer county']['plumbers']=3
>>> a['new jersey']['middlesex county']['programmers']=81
>>> a['new jersey']['mercer county']['programmers']=81
>>> a['new jersey']['middlesex county']['salesmen']=62
>>> a
defaultdict(<function __main__.<lambda>>,
        {'new jersey': defaultdict(<function __main__.<lambda>>,
                     {'mercer county': defaultdict(<function __main__.<lambda>>,
                                  {'plumbers': 3, 'programmers': 81}),
                      'middlesex county': defaultdict(<function __main__.<lambda>>,
                                  {'programmers': 81, 'salesmen': 62})})})

0

Saya dulu menggunakan fungsi ini. ini aman, cepat, mudah dirawat.

def deep_get(dictionary, keys, default=None):
    return reduce(lambda d, key: d.get(key, default) if isinstance(d, dict) else default, keys.split("."), dictionary)

Contoh:

>>> from functools import reduce
>>> def deep_get(dictionary, keys, default=None):
...     return reduce(lambda d, key: d.get(key, default) if isinstance(d, dict) else default, keys.split("."), dictionary)
...
>>> person = {'person':{'name':{'first':'John'}}}
>>> print (deep_get(person, "person.name.first"))
John
>>> print (deep_get(person, "person.name.lastname"))
None
>>> print (deep_get(person, "person.name.lastname", default="No lastname"))
No lastname
>>>
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.