Beberapa tingkat 'collection.defaultdict' dengan Python

176

Berkat beberapa orang hebat di SO, saya menemukan kemungkinan yang ditawarkan oleh collections.defaultdict, terutama dalam keterbacaan dan kecepatan. Saya telah menggunakan mereka dengan sukses.

Sekarang saya ingin mengimplementasikan tiga tingkat kamus, dua yang teratas defaultdictdan yang paling rendah int. Saya tidak menemukan cara yang tepat untuk melakukan ini. Ini usaha saya:

from collections import defaultdict
d = defaultdict(defaultdict)
a = [("key1", {"a1":22, "a2":33}),
     ("key2", {"a1":32, "a2":55}),
     ("key3", {"a1":43, "a2":44})]
for i in a:
    d[i[0]] = i[1]

Sekarang ini berfungsi, tetapi yang berikut, yang merupakan perilaku yang diinginkan, tidak:

d["key4"]["a1"] + 1

Saya menduga bahwa saya seharusnya menyatakan di suatu tempat bahwa tingkat kedua defaultdictadalah tipe int, tetapi saya tidak menemukan di mana atau bagaimana melakukannya.

Alasan saya menggunakan defaultdictdi tempat pertama adalah untuk menghindari keharusan menginisialisasi kamus untuk setiap kunci baru.

Ada saran yang lebih elegan?

Terima kasih pythoneers!

Morlock
sumber

Jawaban:

341

Menggunakan:

from collections import defaultdict
d = defaultdict(lambda: defaultdict(int))

Ini akan membuat yang baru defaultdict(int)setiap kali kunci baru diakses d.

interjay
sumber
2
Satu-satunya masalah adalah itu tidak akan acar, artinya multiprocessingtidak senang mengirim ini bolak-balik.
Noah
19
@Noah: Ini akan acar jika Anda menggunakan fungsi tingkat modul bernama bukan lambda.
interjay
4
@ScienceFriction Apa pun spesifik yang Anda perlu bantuan? Ketika d[new_key]diakses, itu akan memanggil lambda yang akan membuat yang baru defaultdict(int). Dan ketika d[existing_key][new_key2]diakses, baru intakan dibuat.
interjay
11
Ini luar biasa. Sepertinya saya memperbarui sumpah perkawinan saya ke Python setiap hari.
mVChr
3
Mencari rincian lebih lanjut tentang menggunakan metode ini dengan multiprocessingdan apa fungsi tingkat modul bernama itu? Pertanyaan ini menindaklanjuti.
Cecilia
32

Cara lain untuk membuat pickleable, nested defaultdict adalah dengan menggunakan objek parsial alih-alih lambda:

from functools import partial
...
d = defaultdict(partial(defaultdict, int))

Ini akan berfungsi karena kelas defaultdict dapat diakses secara global di tingkat modul:

"Anda tidak dapat mengasup objek parsial kecuali fungsi [atau dalam hal ini, kelas] yang dibungkusnya dapat diakses secara global ... di bawah __name__-nya (dalam __module__)" - Pengawetan fungsi parsial yang dibungkus

Nathaniel Gentile
sumber
12

Lihatlah jawaban nosklo di sini untuk solusi yang lebih umum.

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}}}
miles82
sumber
Terima kasih atas tautan @ miles82 (dan hasil edit, @voyager). Seberapa pythonesque dan amannya pendekatan ini?
Morlock
2
Sayangnya solusi ini tidak mempertahankan bagian paling default dari defaultdict, yang merupakan kekuatan untuk menulis sesuatu seperti D ['key'] + = 1 tanpa khawatir tentang keberadaan kunci tersebut. Itulah fitur utama yang saya gunakan untuk defaultdict ... tapi saya bisa membayangkan kamus yang memperdalam secara dinamis juga sangat berguna.
rschwieb
2
@rschwieb Anda dapat menambahkan kekuatan untuk menulis + = 1 dengan menambahkan metode add .
spazm
5

Sesuai permintaan @ rschwieb untuk D['key'] += 1, kami dapat memperluas sebelumnya dengan mengesampingkan penambahan dengan mendefinisikan __add__metode, untuk membuat ini berperilaku lebih seperticollections.Counter()

Pertama __missing__akan dipanggil untuk membuat nilai kosong baru, yang akan diteruskan __add__. Kami menguji nilainya, mengandalkan nilai kosong menjadi False.

Lihat mengemulasi tipe numerik untuk informasi lebih lanjut tentang mengganti.

from numbers import Number


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

    def __add__(self, x):
        """ override addition for numeric types when self is empty """
        if not self and isinstance(x, Number):
            return x
        raise ValueError

    def __sub__(self, x):
        if not self and isinstance(x, Number):
            return -1 * x
        raise ValueError

Contoh:

>>> import autovivify
>>> a = autovivify.autovivify()
>>> a
{}
>>> a[2]
{}
>>> a
{2: {}}
>>> a[4] += 1
>>> a[5][3][2] -= 1
>>> a
{2: {}, 4: 1, 5: {3: {2: -1}}}

Daripada memeriksa argumen adalah Angka (sangat non-python, amirite!) Kita bisa memberikan nilai 0 default dan kemudian mencoba operasi:

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

    def __add__(self, x):
        """ override addition when self is empty """
        if not self:
            return 0 + x
        raise ValueError

    def __sub__(self, x):
        """ override subtraction when self is empty """
        if not self:
            return 0 - x
        raise ValueError
spazm
sumber
haruskah ini meningkatkan NotImplemented daripada ValueError?
spazm
5

Terlambat ke pesta, tapi untuk kedalaman yang sewenang-wenang, aku baru saja melakukan sesuatu seperti ini:

from collections import defaultdict

class DeepDict(defaultdict):
    def __call__(self):
        return DeepDict(self.default_factory)

Triknya di sini adalah membuat DeepDictinstance itu sendiri sebagai pabrik yang valid untuk membangun nilai yang hilang. Sekarang kita bisa melakukan hal-hal seperti

dd = DeepDict(DeepDict(list))
dd[1][2].extend([3,4])
sum(dd[1][2])  # 7

ddd = DeepDict(DeepDict(DeepDict(list)))
ddd[1][2][3].extend([4,5])
sum(ddd[1][2][3])  # 9
Rad Haring
sumber
1
def _sub_getitem(self, k):
    try:
        # sub.__class__.__bases__[0]
        real_val = self.__class__.mro()[-2].__getitem__(self, k)
        val = '' if real_val is None else real_val
    except Exception:
        val = ''
        real_val = None
    # isinstance(Avoid,dict)也是true,会一直递归死
    if type(val) in (dict, list, str, tuple):
        val = type('Avoid', (type(val),), {'__getitem__': _sub_getitem, 'pop': _sub_pop})(val)
        # 重新赋值当前字典键为返回值,当对其赋值时可回溯
        if all([real_val is not None, isinstance(self, (dict, list)), type(k) is not slice]):
            self[k] = val
    return val


def _sub_pop(self, k=-1):
    try:
        val = self.__class__.mro()[-2].pop(self, k)
        val = '' if val is None else val
    except Exception:
        val = ''
    if type(val) in (dict, list, str, tuple):
        val = type('Avoid', (type(val),), {'__getitem__': _sub_getitem, 'pop': _sub_pop})(val)
    return val


class DefaultDict(dict):
    def __getitem__(self, k):
        return _sub_getitem(self, k)

    def pop(self, k):
        return _sub_pop(self, k)

In[8]: d=DefaultDict()
In[9]: d['a']['b']['c']['d']
Out[9]: ''
In[10]: d['a']="ggggggg"
In[11]: d['a']
Out[11]: 'ggggggg'
In[12]: d['a']['pp']
Out[12]: ''

Tidak ada kesalahan lagi. Tidak peduli berapa banyak level yang bersarang. pop no error juga

dd = DefaultDict ({"1": 333333})

ACE Fly
sumber