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.setdefault
mana 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 Vividict
saya 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.setdefault
bekerja 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 dict
untuk 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 setdefault
solusi lain, dan ada dalam setiap situasi di mana saya membutuhkan perilaku semacam ini.
Vividict
? Misalnya3
danlist
untuk dict dari dict dari daftar yang dapat diisi dengand['primary']['secondary']['tertiary'].append(element)
. Saya dapat mendefinisikan 3 kelas berbeda untuk setiap kedalaman tetapi saya ingin menemukan solusi yang lebih bersih.