Python: defaultdict of defaultdict?

323

Apakah ada cara untuk memiliki defaultdict(defaultdict(int))agar kode berikut berfungsi?

for x in stuff:
    d[x.a][x.b] += x.c_int

dperlu dibangun ad-hoc, tergantung pada x.adan x.belemen.

Saya bisa menggunakan:

for x in stuff:
    d[x.a,x.b] += x.c_int

tapi kemudian saya tidak bisa menggunakan:

d.keys()
d[x.a].keys()
Jonathan
sumber
6
Lihat pertanyaan serupa Apa cara terbaik untuk mengimplementasikan kamus bersarang di Python? . Ada juga beberapa informasi yang mungkin berguna dalam artikel Wikipedia tentang Autovivification .
martineau

Jawaban:

571

Ya seperti ini:

defaultdict(lambda: defaultdict(int))

Argumen a defaultdict(dalam hal ini adalah lambda: defaultdict(int)) akan dipanggil ketika Anda mencoba mengakses kunci yang tidak ada. Nilai pengembaliannya akan ditetapkan sebagai nilai baru dari kunci ini, yang berarti dalam kasus kami nilai d[Key_doesnt_exist]akan defaultdict(int).

Jika Anda mencoba mengakses kunci dari default default terakhir ini yaitu d[Key_doesnt_exist][Key_doesnt_exist]akan mengembalikan 0, yang merupakan nilai pengembalian argumen dari default default yaitu int().

mouad
sumber
7
ini bekerja dengan baik! dapatkah Anda menjelaskan alasan di balik sintaks ini?
Jonathan
37
@ Jonathan: Ya tentu, argumen a defaultdict(dalam hal ini lambda : defaultdict(int)) akan dipanggil ketika Anda mencoba mengakses kunci yang tidak ada dan nilai pengembaliannya akan ditetapkan sebagai nilai baru dari kunci ini yang berarti dalam kasus kami nilai d[Key_dont_exist]akan defaultdict(int), dan jika Anda mencoba mengakses kunci dari defaultdict terakhir ini yaitu d[Key_dont_exist][Key_dont_exist]akan mengembalikan 0 yang merupakan nilai pengembalian argumen terakhir defaultdictyaitu int(), Semoga ini bermanfaat.
mouad
25
Argumen untuk defaultdictmenjadi fungsi. defaultdict(int)adalah kamus, sedangkan lambda: defaultdict(int)fungsi yang mengembalikan kamus.
has2k1
27
@ has2k1 Itu tidak benar. Argumen untuk defaultdict harus dapat dipanggil. Lambda adalah callable.
Niels Bom
2
@ RickyLevi, jika Anda ingin itu berfungsi, Anda bisa mengatakan: defaultdict(lambda: defaultdict(lambda: defaultdict(int)))
darophi
51

Parameter ke konstruktor defaultdict adalah fungsi yang akan dipanggil untuk membangun elemen baru. Jadi mari kita gunakan lambda!

>>> from collections import defaultdict
>>> d = defaultdict(lambda : defaultdict(int))
>>> print d[0]
defaultdict(<type 'int'>, {})
>>> print d[0]["x"]
0

Sejak Python 2.7, ada solusi yang lebih baik lagi menggunakan Counter :

>>> from collections import Counter
>>> c = Counter()
>>> c["goodbye"]+=1
>>> c["and thank you"]=42
>>> c["for the fish"]-=5
>>> c
Counter({'and thank you': 42, 'goodbye': 1, 'for the fish': -5})

Beberapa fitur bonus

>>> c.most_common()[:2]
[('and thank you', 42), ('goodbye', 1)]

Untuk informasi lebih lanjut lihat PyMOTW - Koleksi - tipe data wadah dan Dokumentasi Python - koleksi

Yanjost
sumber
5
Hanya untuk melengkapi lingkaran di sini, Anda ingin menggunakan d = defaultdict(lambda : Counter())daripada d = defaultdict(lambda : defaultdict(int))untuk secara khusus mengatasi masalah seperti yang diajukan sebelumnya.
gtion
3
@gtion Anda hanya bisa menggunakan d = defaultdict(Counter())tidak perlu untuk lambda dalam kasus ini
Deb
3
@ Deb Anda memiliki sedikit kesalahan - menghapus tanda kurung bagian dalam sehingga Anda melewatkan callable, bukan Counterobjek. Itu adalah:d = defaultdict(Counter)
Dillon Davis
29

Saya merasa sedikit lebih elegan untuk digunakan partial:

import functools
dd_int = functools.partial(defaultdict, int)
defaultdict(dd_int)

Tentu saja, ini sama dengan lambda.

Katriel
sumber
1
Parsial juga lebih baik daripada lambda di sini karena dapat diterapkan secara rekursif :) lihat jawaban saya di bawah ini untuk metode pabrik defaultdict bersarang generik.
Campi
@Ampampi Anda tidak perlu parsial untuk aplikasi rekursif, AFAICT
Clément
10

Untuk referensi, dimungkinkan untuk menerapkan defaultdictmetode pabrik bersarang generik melalui:

from collections import defaultdict
from functools import partial
from itertools import repeat


def nested_defaultdict(default_factory, depth=1):
    result = partial(defaultdict, default_factory)
    for _ in repeat(None, depth - 1):
        result = partial(defaultdict, result)
    return result()

Kedalaman menentukan jumlah kamus bersarang sebelum jenis yang didefinisikan default_factorydigunakan. Sebagai contoh:

my_dict = nested_defaultdict(list, 3)
my_dict['a']['b']['c'].append('e')
Campi
sumber
Bisakah Anda memberikan contoh penggunaan? Tidak bekerja seperti yang saya harapkan. ndd = nested_defaultdict(dict) .... ndd['a']['b']['c']['d'] = 'e'throwsKeyError: 'b'
David Marx
Hai David, Anda perlu menentukan kedalaman kamus Anda, dalam contoh Anda 3 (ketika Anda mendefinisikan default_factory menjadi kamus juga. Nested_defaultdict (dict, 3) akan bekerja untuk Anda.
Campi
Ini sangat membantu, terima kasih! Satu hal yang saya perhatikan adalah bahwa ini menciptakan default_dict di depth=0, yang mungkin tidak selalu diinginkan jika kedalamannya tidak diketahui pada saat menelepon. Mudah diperbaiki dengan menambahkan garis if not depth: return default_factory(), di bagian atas fungsi, meskipun mungkin ada solusi yang lebih elegan.
Brendan
9

Jawaban sebelumnya telah membahas cara membuat dua level atau n-level defaultdict. Dalam beberapa kasus Anda ingin yang tak terbatas:

def ddict():
    return defaultdict(ddict)

Pemakaian:

>>> d = ddict()
>>> d[1]['a'][True] = 0.5
>>> d[1]['b'] = 3
>>> import pprint; pprint.pprint(d)
defaultdict(<function ddict at 0x7fcac68bf048>,
            {1: defaultdict(<function ddict at 0x7fcac68bf048>,
                            {'a': defaultdict(<function ddict at 0x7fcac68bf048>,
                                              {True: 0.5}),
                             'b': 3})})
Sejuk
sumber
1
Aku suka ini. Ini sangat sederhana, tetapi sangat berguna. Terima kasih!
rosstex
6

Orang lain telah menjawab dengan benar pertanyaan Anda tentang cara membuat yang berikut ini berfungsi:

for x in stuff:
    d[x.a][x.b] += x.c_int

Alternatifnya adalah menggunakan tuple untuk kunci:

d = defaultdict(int)
for x in stuff:
    d[x.a,x.b] += x.c_int
    # ^^^^^^^ tuple key

Hal yang menyenangkan tentang pendekatan ini adalah sederhana dan dapat dengan mudah diperluas. Jika Anda membutuhkan pemetaan tiga level, gunakan tuple tiga item untuk kunci tersebut.

Steven Rumbalski
sumber
4
Solusi ini berarti tidak mudah untuk mendapatkan semua d [xa], karena Anda perlu mengintrospeksi setiap kunci untuk melihat apakah xa sebagai elemen pertama dari tuple.
Matthew Schinckel
5
Jika Anda ingin bersarang 3 level, maka hanya mendefinisikannya sebagai 3 level: d = defaultdict (lambda: defaultdict (lambda: defaultdict (int)))
Matthew Schinckel