Ratakan kamus bersarang, kunci kompresi

Jawaban:

220

Pada dasarnya dengan cara yang sama Anda akan meratakan daftar bersarang, Anda hanya perlu melakukan pekerjaan ekstra untuk iterate dict dengan kunci / nilai, membuat kunci baru untuk kamus baru Anda dan membuat kamus pada langkah terakhir.

import collections

def flatten(d, parent_key='', sep='_'):
    items = []
    for k, v in d.items():
        new_key = parent_key + sep + k if parent_key else k
        if isinstance(v, collections.MutableMapping):
            items.extend(flatten(v, new_key, sep=sep).items())
        else:
            items.append((new_key, v))
    return dict(items)

>>> flatten({'a': 1, 'c': {'a': 2, 'b': {'x': 5, 'y' : 10}}, 'd': [1, 2, 3]})
{'a': 1, 'c_a': 2, 'c_b_x': 5, 'd': [1, 2, 3], 'c_b_y': 10}
Imran
sumber
7
Jika Anda mengganti isinstancedengan try..exceptblok, ini akan berfungsi untuk pemetaan apa pun, bahkan jika itu bukan berasal dari dict.
Björn Pollex
1
Mengubahnya untuk menguji collections.MutableMappingagar lebih generik. Tetapi untuk Python <2.6, try..exceptmungkin merupakan pilihan terbaik.
Imran
5
Jika Anda ingin kamus kosong yang diawetkan dalam versi yang diratakan, Anda mungkin ingin mengubah if isinstance(v, collections.MutableMapping):keif v and isinstance(v, collections.MutableMapping):
tarequeh
3
Perhatikan bahwa new_key = parent_key + sep + k if parent_key else kmengasumsikan bahwa kunci selalu berupa string, jika tidak maka akan dinaikkan TypeError: cannot concatenate 'str' and [other] objects. Namun, Anda bisa memperbaikinya hanya dengan memaksa kke string ( str(k)), atau menyatukan kunci menjadi tuple dan bukannya string (tuple juga bisa menjadi kunci dict).
Scott H
1
Dan fungsi mengembang di sini
mitch
65

Ada dua pertimbangan besar yang perlu dipertimbangkan poster asli:

  1. Apakah ada masalah clobbering keyspace? Misalnya, {'a_b':{'c':1}, 'a':{'b_c':2}}akan menghasilkan {'a_b_c':???}. Solusi di bawah ini menghindari masalah dengan mengembalikan pasangan yang dapat diubah.
  2. Jika kinerja adalah masalah, apakah fungsi kunci-peredam (yang saya sebut sebagai 'bergabung') memerlukan akses ke seluruh jalur-kunci, atau bisakah itu hanya melakukan O (1) bekerja di setiap node di pohon? Jika Anda ingin dapat mengatakannya joinedKey = '_'.join(*keys), itu akan menghabiskan waktu O (N ^ 2). Namun jika Anda mau mengatakan nextKey = previousKey+'_'+thisKey, itu membuat Anda O (N) waktu. Solusi di bawah ini memungkinkan Anda melakukan keduanya (karena Anda hanya bisa menggabungkan semua kunci, lalu mempostingnya).

(Kinerja sepertinya bukan masalah, tapi saya akan menguraikan poin kedua jika ada orang yang peduli: Dalam menerapkan ini, ada banyak pilihan berbahaya. Jika Anda melakukan ini secara rekursif dan menghasilkan dan menghasilkan kembali, atau apa pun yang setara yang menyentuh node lebih dari sekali (yang cukup mudah untuk sengaja melakukan), Anda melakukan berpotensi O (N ^ 2) pekerjaan daripada O (N). Hal ini karena mungkin Anda menghitung kunci akemudian a_1kemudian a_1_i..., dan kemudian menghitung akemudian a_1kemudian a_1_ii..., tapi benar-benar Anda tidak harus menghitung a_1lagi. Bahkan jika Anda tidak menghitung ulang itu, re-menghasilkan itu (pendekatan 'tingkat-by-level') adalah sama buruknya. misalnya A yang baik adalah untuk memikirkan kinerja pada {1:{1:{1:{1:...(N times)...{1:SOME_LARGE_DICTIONARY_OF_SIZE_N}...}}}})

Di bawah ini adalah fungsi yang saya tulis flattenDict(d, join=..., lift=...)yang dapat disesuaikan dengan banyak tujuan dan dapat melakukan apa yang Anda inginkan. Sayangnya itu cukup sulit untuk membuat versi malas dari fungsi ini tanpa menimbulkan hukuman kinerja di atas (banyak python builtins seperti chain.from_iterable sebenarnya tidak efisien, yang saya baru sadari setelah pengujian ekstensif dari tiga versi berbeda dari kode ini sebelum memutuskan yang ini).

from collections import Mapping
from itertools import chain
from operator import add

_FLAG_FIRST = object()

def flattenDict(d, join=add, lift=lambda x:x):
    results = []
    def visit(subdict, results, partialKey):
        for k,v in subdict.items():
            newKey = lift(k) if partialKey==_FLAG_FIRST else join(partialKey,lift(k))
            if isinstance(v,Mapping):
                visit(v, results, newKey)
            else:
                results.append((newKey,v))
    visit(d, results, _FLAG_FIRST)
    return results

Untuk lebih memahami apa yang terjadi, di bawah ini adalah diagram untuk mereka yang tidak terbiasa dengan reduce(kiri), atau dikenal sebagai "flip kiri". Terkadang digambar dengan nilai awal menggantikan k0 (bukan bagian dari daftar, diteruskan ke fungsi). Di sini, Jadalah joinfungsi kita . Kami memproses setiap k n dengan lift(k).

               [k0,k1,...,kN].foldleft(J)
                           /    \
                         ...    kN
                         /
       J(k0,J(k1,J(k2,k3)))
                       /  \
                      /    \
           J(J(k0,k1),k2)   k3
                    /   \
                   /     \
             J(k0,k1)    k2
                 /  \
                /    \
               k0     k1

Ini sebenarnya sama dengan functools.reduce, tetapi di mana fungsi kami melakukan ini untuk semua jalur kunci dari pohon.

>>> reduce(lambda a,b:(a,b), range(5))
((((0, 1), 2), 3), 4)

Demonstrasi (yang seharusnya saya masukkan ke dalam docstring):

>>> testData = {
        'a':1,
        'b':2,
        'c':{
            'aa':11,
            'bb':22,
            'cc':{
                'aaa':111
            }
        }
    }
from pprint import pprint as pp

>>> pp(dict( flattenDict(testData, lift=lambda x:(x,)) ))
{('a',): 1,
 ('b',): 2,
 ('c', 'aa'): 11,
 ('c', 'bb'): 22,
 ('c', 'cc', 'aaa'): 111}

>>> pp(dict( flattenDict(testData, join=lambda a,b:a+'_'+b) ))
{'a': 1, 'b': 2, 'c_aa': 11, 'c_bb': 22, 'c_cc_aaa': 111}    

>>> pp(dict( (v,k) for k,v in flattenDict(testData, lift=hash, join=lambda a,b:hash((a,b))) ))
{1: 12416037344,
 2: 12544037731,
 11: 5470935132935744593,
 22: 4885734186131977315,
 111: 3461911260025554326}

Kinerja:

from functools import reduce
def makeEvilDict(n):
    return reduce(lambda acc,x:{x:acc}, [{i:0 for i in range(n)}]+range(n))

import timeit
def time(runnable):
    t0 = timeit.default_timer()
    _ = runnable()
    t1 = timeit.default_timer()
    print('took {:.2f} seconds'.format(t1-t0))

>>> pp(makeEvilDict(8))
{7: {6: {5: {4: {3: {2: {1: {0: {0: 0,
                                 1: 0,
                                 2: 0,
                                 3: 0,
                                 4: 0,
                                 5: 0,
                                 6: 0,
                                 7: 0}}}}}}}}}

import sys
sys.setrecursionlimit(1000000)

forget = lambda a,b:''

>>> time(lambda: dict(flattenDict(makeEvilDict(10000), join=forget)) )
took 0.10 seconds
>>> time(lambda: dict(flattenDict(makeEvilDict(100000), join=forget)) )
[1]    12569 segmentation fault  python

... huh, jangan pikir itu salahku ...


[catatan sejarah yang tidak penting karena masalah moderasi]

Mengenai dugaan duplikat Flatten, kamus kamus (sedalam 2 level) dari daftar dengan Python :

Solusi pertanyaan itu dapat diimplementasikan dalam hal ini dengan melakukan sorted( sum(flatten(...),[]) ). Sebaliknya tidak mungkin: meskipun benar bahwa nilai-nilai dari flatten(...)dapat pulih dari dugaan duplikat dengan pemetaan tingkat tinggi akumulator, seseorang tidak dapat memulihkan kunci. (sunting: Juga ternyata pertanyaan pemilik duplikat dugaan itu benar-benar berbeda, karena hanya membahas kamus dengan kedalaman 2 tingkat, meskipun salah satu jawaban di halaman itu memberikan solusi umum.)

ninjagecko
sumber
2
Saya tidak yakin apakah ini relevan dengan pertanyaan. Solusi ini tidak meratakan item kamus dari daftar kamus, yaitu {'a': [{'aa': 1}, {'ab': 2}]}. Fungsi flattenDict dapat diubah dengan mudah untuk mengakomodasi kasus ini.
Stewbaca
55

Atau jika Anda sudah menggunakan panda, Anda dapat melakukannya dengan json_normalize()seperti:

import pandas as pd

d = {'a': 1,
     'c': {'a': 2, 'b': {'x': 5, 'y' : 10}},
     'd': [1, 2, 3]}

df = pd.io.json.json_normalize(d, sep='_')

print(df.to_dict(orient='records')[0])

Keluaran:

{'a': 1, 'c_a': 2, 'c_b_x': 5, 'c_b_y': 10, 'd': [1, 2, 3]}
MYGz
sumber
4
atau hanya melewati argumen sep :)
Blue Moon
2
Sedikit memalukan itu tidak menangani daftar :)
Roelant
31

Jika Anda menggunakan pandasada fungsi tersembunyi di pandas.io.json._normalize1 disebut nested_to_recordyang melakukan ini dengan tepat.

from pandas.io.json._normalize import nested_to_record    

flat = nested_to_record(my_dict, sep='_')

1 Dalam versi panda 0.24.xdan penggunaan yang lebih lama pandas.io.json.normalize(tanpa _)

Aaron N. Brock
sumber
1
Apa yang berhasil untuk saya adalah from pandas.io.json._normalize import nested_to_record. Perhatikan garis bawah ( _) sebelumnya normalize.
Eyal Levin
2
@EyalLevin Tangkapan yang bagus! Ini berubah 0.25.x, saya sudah memperbarui jawabannya. :)
Aaron N. Brock
28

Berikut adalah semacam implementasi "fungsional", "satu-liner". Itu adalah rekursif, dan berdasarkan pada ekspresi kondisional dan pemahaman dict.

def flatten_dict(dd, separator='_', prefix=''):
    return { prefix + separator + k if prefix else k : v
             for kk, vv in dd.items()
             for k, v in flatten_dict(vv, separator, kk).items()
             } if isinstance(dd, dict) else { prefix : dd }

Uji:

In [2]: flatten_dict({'abc':123, 'hgf':{'gh':432, 'yu':433}, 'gfd':902, 'xzxzxz':{"432":{'0b0b0b':231}, "43234":1321}}, '.')
Out[2]: 
{'abc': 123,
 'gfd': 902,
 'hgf.gh': 432,
 'hgf.yu': 433,
 'xzxzxz.432.0b0b0b': 231,
 'xzxzxz.43234': 1321}
Dibagi nol
sumber
Ini tidak berfungsi untuk kamus umum, khususnya, dengan kunci tuple, misalnya pengganti ('hgf',2)kunci 2 dalam lemparan tes AndaTypeError
alancalvitti
@alancalvitti Ini menganggapnya sebagai string, atau sesuatu yang mendukung +operator. Untuk hal lain, Anda harus beradaptasi prefix + separator + kdengan pemanggilan fungsi yang sesuai untuk menyusun objek.
dividebyzero
Masalah lain yang relevan dengan kunci tuple. Saya telah memposting secara terpisah cara menggeneralisasi berdasarkan metode Anda. Namun itu tidak dapat dengan benar menangani contoh {'a_b':{'c':1}, 'a':{'b_c':2}}
ninjageko
2
Saya mulai khawatir, tidak melihat jawaban menggunakan rekursi. Apa yang salah dengan pemuda kita hari ini?
Jakov
tidak melakukan apa-apa jika sebuah dikt memiliki daftar dict, seperti ini:{'name': 'Steven', 'children': [{'name': 'Jessica', 'children': []}, {'name': 'George', 'children': []}]}
Gergely M
12

Kode:

test = {'a': 1, 'c': {'a': 2, 'b': {'x': 5, 'y' : 10}}, 'd': [1, 2, 3]}

def parse_dict(init, lkey=''):
    ret = {}
    for rkey,val in init.items():
        key = lkey+rkey
        if isinstance(val, dict):
            ret.update(parse_dict(val, key+'_'))
        else:
            ret[key] = val
    return ret

print(parse_dict(test,''))

Hasil:

$ python test.py
{'a': 1, 'c_a': 2, 'c_b_x': 5, 'd': [1, 2, 3], 'c_b_y': 10}

Saya menggunakan python3.2, perbarui untuk versi python Anda.

Pavan Yalamanchili
sumber
Anda mungkin ingin menentukan nilai default lkey=''dalam definisi fungsi Anda alih-alih saat memanggil fungsi. Lihat jawaban lain dalam hal ini.
Acumenus
6

Bagaimana dengan solusi fungsional dan performan di Python3.5?

from functools import reduce


def _reducer(items, key, val, pref):
    if isinstance(val, dict):
        return {**items, **flatten(val, pref + key)}
    else:
        return {**items, pref + key: val}

def flatten(d, pref=''):
    return(reduce(
        lambda new_d, kv: _reducer(new_d, *kv, pref), 
        d.items(), 
        {}
    ))

Ini bahkan lebih berkinerja:

def flatten(d, pref=''):
    return(reduce(
        lambda new_d, kv: \
            isinstance(kv[1], dict) and \
            {**new_d, **flatten(kv[1], pref + kv[0])} or \
            {**new_d, pref + kv[0]: kv[1]}, 
        d.items(), 
        {}
    ))

Digunakan:

my_obj = {'a': 1, 'c': {'a': 2, 'b': {'x': 5, 'y': 10}}, 'd': [1, 2, 3]}

print(flatten(my_obj)) 
# {'d': [1, 2, 3], 'cby': 10, 'cbx': 5, 'ca': 2, 'a': 1}
Rotareti
sumber
2
Bagaimana dengan solusi yang bisa dibaca dan berfungsi? ;) Pada versi mana Anda menguji ini? Saya Mendapatkan "Kesalahan sintaks" saat mencoba ini di Python 3.4.3. Tampaknya penggunaan "** semua" tidak sah.
Ingo Fischer
Saya bekerja sejak Python 3.5. Tidak tahu itu tidak berfungsi dengan 3.4. Anda benar ini tidak mudah dibaca. Saya memperbarui jawabannya. Semoga lebih mudah dibaca sekarang. :)
Rotareti
1
Ditambahkan hilang mengurangi impor. Masih menemukan kode yang sulit untuk dipahami dan saya pikir itu adalah contoh yang baik mengapa Guido van Rossum sendiri mengecilkan penggunaan lambda, mengurangi, memfilter dan memetakan pada tahun 2005: artima.com/weblogs/viewpost.jsp?thread=98196
Ingo Fischer
Saya setuju. Python tidak benar-benar dirancang untuk pemrograman fungsional . Masih saya pikir reducesangat bagus jika Anda perlu mengurangi kamus. Saya memperbarui jawabannya. Seharusnya terlihat sedikit lebih pythonic sekarang.
Rotareti
6

Ini tidak terbatas pada kamus, tetapi setiap tipe pemetaan yang mengimplementasikan .items (). Lebih lanjut lebih cepat karena menghindari kondisi jika. Namun demikian kredit jatuh ke Imran:

def flatten(d, parent_key=''):
    items = []
    for k, v in d.items():
        try:
            items.extend(flatten(v, '%s%s_' % (parent_key, k)).items())
        except AttributeError:
            items.append(('%s%s' % (parent_key, k), v))
    return dict(items)
Davoud Taghawi-Nejad
sumber
1
Jika dbukan dicttipe pemetaan khusus yang tidak diterapkan items, fungsi Anda akan gagal saat itu juga. Jadi, itu tidak bekerja untuk setiap jenis pemetaan tetapi hanya yang menerapkan items().
user6037143
@ user6037143 apakah Anda pernah menemukan tipe pemetaan yang tidak diterapkan items? Saya ingin tahu melihatnya.
Trey Hunner
1
@ user6037143, tidak, Anda tidak memiliki definisi jika item tidak diterapkan itu bukan tipe pemetaan.
Davoud Taghawi-Nejad
@ DavoudTaghawi-Nejad, dapatkah Anda memodifikasi ini untuk menangani kunci umum misalnya tupel yang tidak boleh diratakan secara internal.
alancalvitti
5

Solusi Python 3.3 saya menggunakan generator:

def flattenit(pyobj, keystring=''):
   if type(pyobj) is dict:
     if (type(pyobj) is dict):
         keystring = keystring + "_" if keystring else keystring
         for k in pyobj:
             yield from flattenit(pyobj[k], keystring + k)
     elif (type(pyobj) is list):
         for lelm in pyobj:
             yield from flatten(lelm, keystring)
   else:
      yield keystring, pyobj

my_obj = {'a': 1, 'c': {'a': 2, 'b': {'x': 5, 'y': 10}}, 'd': [1, 2, 3]}

#your flattened dictionary object
flattened={k:v for k,v in flattenit(my_obj)}
print(flattened)

# result: {'c_b_y': 10, 'd': [1, 2, 3], 'c_a': 2, 'a': 1, 'c_b_x': 5}
Atul
sumber
dapatkah Anda memperluas untuk menangani jenis kunci apa pun yang valid selain str (termasuk tuple)? Alih-alih merangkai string, bergabunglah dengan mereka dalam tuple.
alancalvitti
4

Fungsi sederhana untuk meratakan kamus bersarang. Untuk Python 3, ganti .iteritems()dengan.items()

def flatten_dict(init_dict):
    res_dict = {}
    if type(init_dict) is not dict:
        return res_dict

    for k, v in init_dict.iteritems():
        if type(v) == dict:
            res_dict.update(flatten_dict(v))
        else:
            res_dict[k] = v

    return res_dict

Gagasan / persyaratannya adalah: Dapatkan kamus datar tanpa kunci induk.

Contoh penggunaan:

dd = {'a': 3, 
      'b': {'c': 4, 'd': 5}, 
      'e': {'f': 
                 {'g': 1, 'h': 2}
           }, 
      'i': 9,
     }

flatten_dict(dd)

>> {'a': 3, 'c': 4, 'd': 5, 'g': 1, 'h': 2, 'i': 9}

Menjaga kunci induk juga sederhana.

Ivy Tumbuh
sumber
4

Memanfaatkan rekursi, menjaganya tetap sederhana dan mudah dibaca manusia:

def flatten_dict(dictionary, accumulator=None, parent_key=None, separator="."):
    if accumulator is None:
        accumulator = {}

    for k, v in dictionary.items():
        k = f"{parent_key}{separator}{k}" if parent_key else k
        if isinstance(v, dict):
            flatten_dict(dictionary=v, accumulator=accumulator, parent_key=k)
            continue

        accumulator[k] = v

    return accumulator

Panggilan itu sederhana:

new_dict = flatten_dict(dictionary)

atau

new_dict = flatten_dict(dictionary, separator="_")

jika kita ingin mengubah pemisah default.

Sedikit gangguan:

Ketika fungsi pertama kali dipanggil, itu disebut hanya melewati dictionarykita ingin meratakan. The accumulatorparameter di sini untuk dukungan rekursi, yang kita lihat nanti. Jadi, kami instantiate accumulatorke kamus kosong di mana kami akan meletakkan semua nilai yang bersarang dari aslinya dictionary.

if accumulator is None:
    accumulator = {}

Saat kami mengulangi nilai kamus, kami membuat kunci untuk setiap nilai. The parent_keyArgumen akan Noneuntuk panggilan pertama, sedangkan untuk setiap kamus bersarang, itu akan berisi kunci menunjuk ke sana, jadi kami tambahkan kunci itu.

k = f"{parent_key}{separator}{k}" if parent_key else k

Dalam hal nilai vyang kditunjuk oleh kuncinya adalah kamus, fungsinya memanggil dirinya sendiri, melewati kamus bersarang, accumulator(yang diteruskan dengan referensi, jadi semua perubahan yang dilakukan untuk itu dilakukan pada contoh yang sama) dan kunci ksehingga kita dapat membuat kunci gabungan. Perhatikan continuepernyataan itu. Kami ingin melewati baris berikutnya, di luar ifblok, sehingga kamus bersarang tidak berakhir di tombol accumulatorbawah k.

if isinstance(v, dict):
    flatten_dict(dict=v, accumulator=accumulator, parent_key=k)
    continue

Jadi, apa yang kita lakukan seandainya nilainya vbukan kamus? Masukkan saja tidak berubah di dalam accumulator.

accumulator[k] = v

Setelah kami selesai kami hanya mengembalikan accumulator, meninggalkan dictionaryargumen asli tidak tersentuh.

CATATAN

Ini hanya akan berfungsi dengan kamus yang memiliki string sebagai kunci. Ini akan bekerja dengan objek hash yang mengimplementasikan __repr__metode, tetapi akan menghasilkan hasil yang tidak diinginkan.

Jakov
sumber
3

Ini mirip dengan jawaban imran dan ralu. Itu tidak menggunakan generator, tetapi mempekerjakan rekursi dengan penutupan:

def flatten_dict(d, separator='_'):
  final = {}
  def _flatten_dict(obj, parent_keys=[]):
    for k, v in obj.iteritems():
      if isinstance(v, dict):
        _flatten_dict(v, parent_keys + [k])
      else:
        key = separator.join(parent_keys + [k])
        final[key] = v
  _flatten_dict(d)
  return final

>>> print flatten_dict({'a': 1, 'c': {'a': 2, 'b': {'x': 5, 'y' : 10}}, 'd': [1, 2, 3]})
{'a': 1, 'c_a': 2, 'c_b_x': 5, 'd': [1, 2, 3], 'c_b_y': 10}
Jonathan Drake
sumber
Saya tidak yakin apakah menggunakan istilah " closure " benar di sini, karena fungsinya _flatten_dicttidak pernah dikembalikan, juga tidak diharapkan akan pernah dikembalikan. Ini mungkin bisa disebut sebagai subfungsi atau fungsi tertutup sebagai gantinya.
Acumenus
3

Solusi Davoud sangat bagus tetapi tidak memberikan hasil yang memuaskan ketika dict bersarang juga berisi daftar dict, tetapi kodenya diadaptasi untuk kasus tersebut:

def flatten_dict(d):
    items = []
    for k, v in d.items():
        try:
            if (type(v)==type([])): 
                for l in v: items.extend(flatten_dict(l).items())
            else: 
                items.extend(flatten_dict(v).items())
        except AttributeError:
            items.append((k, v))
    return dict(items)
pengguna3830731
sumber
Anda dapat menyimpan hasil type([])untuk menghindari panggilan fungsi untuk setiap item dict.
bfontaine
2
Silakan gunakan isinstance(v, list)sebagai gantinya
Druska
2

Jawaban di atas bekerja dengan sangat baik. Hanya berpikir saya akan menambahkan fungsi tidak rata yang saya tulis:

def unflatten(d):
    ud = {}
    for k, v in d.items():
        context = ud
        for sub_key in k.split('_')[:-1]:
            if sub_key not in context:
                context[sub_key] = {}
            context = context[sub_key]
        context[k.split('_')[-1]] = v
    return ud

Catatan: Ini tidak memperhitungkan '_' yang sudah ada di kunci, sama seperti rekan-rekan rata.

tarequeh
sumber
2

Berikut adalah algoritma untuk penggantian yang elegan dan di tempat. Diuji dengan Python 2.7 dan Python 3.5. Menggunakan karakter titik sebagai pemisah.

def flatten_json(json):
    if type(json) == dict:
        for k, v in list(json.items()):
            if type(v) == dict:
                flatten_json(v)
                json.pop(k)
                for k2, v2 in v.items():
                    json[k+"."+k2] = v2

Contoh:

d = {'a': {'b': 'c'}}                   
flatten_json(d)
print(d)
unflatten_json(d)
print(d)

Keluaran:

{'a.b': 'c'}
{'a': {'b': 'c'}}

Saya menerbitkan kode ini di sini bersama dengan unflatten_jsonfungsi yang cocok .

Alexander Ryzhov
sumber
2

Jika Anda ingin flat bersarang kamus dan ingin semua daftar kunci unik maka di sini adalah solusinya:

def flat_dict_return_unique_key(data, unique_keys=set()):
    if isinstance(data, dict):
        [unique_keys.add(i) for i in data.keys()]
        for each_v in data.values():
            if isinstance(each_v, dict):
                flat_dict_return_unique_key(each_v, unique_keys)
    return list(set(unique_keys))
Ranvijay Sachan
sumber
2
def flatten(unflattened_dict, separator='_'):
    flattened_dict = {}

    for k, v in unflattened_dict.items():
        if isinstance(v, dict):
            sub_flattened_dict = flatten(v, separator)
            for k2, v2 in sub_flattened_dict.items():
                flattened_dict[k + separator + k2] = v2
        else:
            flattened_dict[k] = v

    return flattened_dict
Pari Rajaram
sumber
2
def flatten_nested_dict(_dict, _str=''):
    '''
    recursive function to flatten a nested dictionary json
    '''
    ret_dict = {}
    for k, v in _dict.items():
        if isinstance(v, dict):
            ret_dict.update(flatten_nested_dict(v, _str = '_'.join([_str, k]).strip('_')))
        elif isinstance(v, list):
            for index, item in enumerate(v):
                if isinstance(item, dict):
                    ret_dict.update(flatten_nested_dict(item,  _str= '_'.join([_str, k, str(index)]).strip('_')))
                else:
                    ret_dict['_'.join([_str, k, str(index)]).strip('_')] = item
        else:
            ret_dict['_'.join([_str, k]).strip('_')] = v
    return ret_dict
Pradeep Pathak
sumber
ini berfungsi dengan daftar di dalam dict kami yang bersarang, tetapi tidak memiliki opsi pemisah khusus
Nikhil VJ
2

Saya sedang memikirkan subkelas UserDict untuk secara otomatis menyamakan kunci.

class FlatDict(UserDict):
    def __init__(self, *args, separator='.', **kwargs):
        self.separator = separator
        super().__init__(*args, **kwargs)

    def __setitem__(self, key, value):
        if isinstance(value, dict):
            for k1, v1 in FlatDict(value, separator=self.separator).items():
                super().__setitem__(f"{key}{self.separator}{k1}", v1)
        else:
            super().__setitem__(key, value)

‌ Keuntungan bahwa kunci dapat ditambahkan dengan cepat, atau menggunakan instanciation standar, tanpa kejutan:

>>> fd = FlatDict(
...    {
...        'person': {
...            'sexe': 'male', 
...            'name': {
...                'first': 'jacques',
...                'last': 'dupond'
...            }
...        }
...    }
... )
>>> fd
{'person.sexe': 'male', 'person.name.first': 'jacques', 'person.name.last': 'dupond'}
>>> fd['person'] = {'name': {'nickname': 'Bob'}}
>>> fd
{'person.sexe': 'male', 'person.name.first': 'jacques', 'person.name.last': 'dupond', 'person.name.nickname': 'Bob'}
>>> fd['person.name'] = {'civility': 'Dr'}
>>> fd
{'person.sexe': 'male', 'person.name.first': 'jacques', 'person.name.last': 'dupond', 'person.name.nickname': 'Bob', 'person.name.civility': 'Dr'}
loutre
sumber
1
Menugaskan ke fd ['orang'] tetapi mempertahankan nilai yang ada cukup mengejutkan. Itu bukan cara kerja dikte reguler.
tbm
1

Menggunakan generator:

def flat_dic_helper(prepand,d):
    if len(prepand) > 0:
        prepand = prepand + "_"
    for k in d:
        i=d[k]
        if type(i).__name__=='dict':
            r = flat_dic_helper(prepand+k,i)
            for j in r:
                yield j
        else:
            yield (prepand+k,i)

def flat_dic(d): return dict(flat_dic_helper("",d))

d={'a': 1, 'c': {'a': 2, 'b': {'x': 5, 'y' : 10}}, 'd': [1, 2, 3]}
print(flat_dic(d))


>> {'a': 1, 'c_a': 2, 'c_b_x': 5, 'd': [1, 2, 3], 'c_b_y': 10}
Luka Rahne
sumber
2
type(i).__name__=='dict'dapat diganti dengan type(i) is dictatau mungkin lebih baik isinstance(d, dict)(atau Mapping/ MutableMapping).
Cristian Ciupitu
1

Menggunakan dict.popitem () dalam rekursi seperti daftar-langsung:

def flatten(d):
    if d == {}:
        return d
    else:
        k,v = d.popitem()
        if (dict != type(v)):
            return {k:v, **flatten(d)}
        else:
            flat_kv = flatten(v)
            for k1 in list(flat_kv.keys()):
                flat_kv[k + '_' + k1] = flat_kv[k1]
                del flat_kv[k1]
            return {**flat_kv, **flatten(d)}
Fredaka
sumber
1

Tidak persis apa yang diminta OP, tetapi banyak orang datang ke sini mencari cara untuk meratakan data JSON bersarang dunia nyata yang dapat memiliki objek json dan array bernilai-nilai kunci dan objek json di dalam array dan sebagainya. JSON tidak termasuk tupel, jadi kami tidak perlu khawatir.

Saya menemukan implementasi dari daftar-inklusi komentar oleh @roneo ke jawaban yang diposting oleh @Imran :

https://github.com/ScriptSmith/socialreaper/blob/master/socialreaper/tools.py#L8

import collections
def flatten(dictionary, parent_key=False, separator='.'):
    """
    Turn a nested dictionary into a flattened dictionary
    :param dictionary: The dictionary to flatten
    :param parent_key: The string to prepend to dictionary's keys
    :param separator: The string used to separate flattened keys
    :return: A flattened dictionary
    """

    items = []
    for key, value in dictionary.items():
        new_key = str(parent_key) + separator + key if parent_key else key
        if isinstance(value, collections.MutableMapping):
            items.extend(flatten(value, new_key, separator).items())
        elif isinstance(value, list):
            for k, v in enumerate(value):
                items.extend(flatten({str(k): v}, new_key).items())
        else:
            items.append((new_key, value))
    return dict(items)

Menguji:

flatten({'a': 1, 'c': {'a': 2, 'b': {'x': 5, 'y' : 10}}, 'd': [1, 2, 3] })

>> {'a': 1, 'c.a': 2, 'c.b.x': 5, 'c.b.y': 10, 'd.0': 1, 'd.1': 2, 'd.2': 3}

Annd yang melakukan pekerjaan yang perlu saya lakukan: Saya membuang json yang rumit ini dan meratakannya untuk saya.

Semua kredit ke https://github.com/ScriptSmith .

Nikhil VJ
sumber
1

Saya benar-benar menulis paket yang disebut cherrypicker baru-baru ini untuk menangani hal-hal yang persis seperti ini karena saya harus sering melakukannya!

Saya pikir kode berikut akan memberi Anda apa yang Anda cari:

from cherrypicker import CherryPicker

dct = {
    'a': 1,
    'c': {
        'a': 2,
        'b': {
            'x': 5,
            'y' : 10
        }
    },
    'd': [1, 2, 3]
}

picker = CherryPicker(dct)
picker.flatten().get()

Anda dapat menginstal paket dengan:

pip install cherrypicker

... dan ada lebih banyak dokumen dan panduan di https://cherrypicker.readthedocs.io .

Metode lain mungkin lebih cepat, tetapi prioritas paket ini adalah untuk membuat tugas-tugas seperti itu mudah . Jika Anda memiliki daftar objek yang besar untuk diratakan, Anda juga dapat memberitahu CherryPicker untuk menggunakan pemrosesan paralel untuk mempercepat.

besar-o
sumber
Saya suka pendekatan alternatif.
Gergely M
0

Saya selalu lebih suka mengakses dictobjek melalui .items(), jadi untuk mendatarkan dicts saya menggunakan generator rekursif berikut flat_items(d). Jika Anda ingin memilikinya dictlagi, cukup bungkus seperti ini:flat = dict(flat_items(d))

def flat_items(d, key_separator='.'):
    """
    Flattens the dictionary containing other dictionaries like here: /programming/6027558/flatten-nested-python-dictionaries-compressing-keys

    >>> example = {'a': 1, 'c': {'a': 2, 'b': {'x': 5, 'y' : 10}}, 'd': [1, 2, 3]}
    >>> flat = dict(flat_items(example, key_separator='_'))
    >>> assert flat['c_b_y'] == 10
    """
    for k, v in d.items():
        if type(v) is dict:
            for k1, v1 in flat_items(v, key_separator=key_separator):
                yield key_separator.join((k, k1)), v1
        else:
            yield k, v
Vladimir Ignatyev
sumber
0

Variasi kamus bersarang rata ini, mengompres kunci dengan max_level dan peredam khusus.

  def flatten(d, max_level=None, reducer='tuple'):
      if reducer == 'tuple':
          reducer_seed = tuple()
          reducer_func = lambda x, y: (*x, y)
      else:
          raise ValueError(f'Unknown reducer: {reducer}')

      def impl(d, pref, level):
        return reduce(
            lambda new_d, kv:
                (max_level is None or level < max_level)
                and isinstance(kv[1], dict)
                and {**new_d, **impl(kv[1], reducer_func(pref, kv[0]), level + 1)}
                or {**new_d, reducer_func(pref, kv[0]): kv[1]},
                d.items(),
            {}
        )

      return impl(d, reducer_seed, 0)
pengguna2528473
sumber
0

Jika Anda tidak keberatan dengan fungsi rekursif, berikut ini solusinya. Saya juga telah mengambil kebebasan untuk memasukkan pengecualian parameter- jika ada satu atau lebih nilai yang ingin Anda pertahankan.

Kode:

def flatten_dict(dictionary, exclude = [], delimiter ='_'):
    flat_dict = dict()
    for key, value in dictionary.items():
        if isinstance(value, dict) and key not in exclude:
            flatten_value_dict = flatten_dict(value, exclude, delimiter)
            for k, v in flatten_value_dict.items():
                flat_dict[f"{key}{delimiter}{k}"] = v
        else:
            flat_dict[key] = value
    return flat_dict

Pemakaian:

d = {'a':1, 'b':[1, 2], 'c':3, 'd':{'a':4, 'b':{'a':7, 'b':8}, 'c':6}, 'e':{'a':1,'b':2}}
flat_d = flatten_dict(dictionary=d, exclude=['e'], delimiter='.')
print(flat_d)

Keluaran:

{'a': 1, 'b': [1, 2], 'c': 3, 'd.a': 4, 'd.b.a': 7, 'd.b.b': 8, 'd.c': 6, 'e': {'a': 1, 'b': 2}}
Thomas
sumber
0

Saya mencoba beberapa solusi di halaman ini - meskipun tidak semua - tetapi yang saya coba gagal menangani daftar dict yang bersarang.

Pertimbangkan dict seperti ini:

d = {
        'owner': {
            'name': {'first_name': 'Steven', 'last_name': 'Smith'},
            'lottery_nums': [1, 2, 3, 'four', '11', None],
            'address': {},
            'tuple': (1, 2, 'three'),
            'tuple_with_dict': (1, 2, 'three', {'is_valid': False}),
            'set': {1, 2, 3, 4, 'five'},
            'children': [
                {'name': {'first_name': 'Jessica',
                          'last_name': 'Smith', },
                 'children': []
                 },
                {'name': {'first_name': 'George',
                          'last_name': 'Smith'},
                 'children': []
                 }
            ]
        }
    }

Inilah solusi darurat saya:

def flatten_dict(input_node: dict, key_: str = '', output_dict: dict = {}):
    if isinstance(input_node, dict):
        for key, val in input_node.items():
            new_key = f"{key_}.{key}" if key_ else f"{key}"
            flatten_dict(val, new_key, output_dict)
    elif isinstance(input_node, list):
        for idx, item in enumerate(input_node):
            flatten_dict(item, f"{key_}.{idx}", output_dict)
    else:
        output_dict[key_] = input_node
    return output_dict

yang menghasilkan:

{
  owner.name.first_name: Steven,
  owner.name.last_name: Smith,
  owner.lottery_nums.0: 1,
  owner.lottery_nums.1: 2,
  owner.lottery_nums.2: 3,
  owner.lottery_nums.3: four,
  owner.lottery_nums.4: 11,
  owner.lottery_nums.5: None,
  owner.tuple: (1, 2, 'three'),
  owner.tuple_with_dict: (1, 2, 'three', {'is_valid': False}),
  owner.set: {1, 2, 3, 4, 'five'},
  owner.children.0.name.first_name: Jessica,
  owner.children.0.name.last_name: Smith,
  owner.children.1.name.first_name: George,
  owner.children.1.name.last_name: Smith,
}

Solusi sementara dan itu tidak sempurna.
CATATAN:

  • itu tidak menyimpan dikte kosong seperti pasangan address: {}k / v.

  • itu tidak akan meratakan dicts di tuple bersarang - meskipun akan mudah untuk menambahkan menggunakan fakta bahwa tuple python bertindak mirip dengan daftar.

Sangat M
sumber
-1

Cukup gunakan python-benedict, ini adalah subclass dict yang menawarkan banyak fitur, termasuk flattenmetode. Mungkin untuk menginstalnya menggunakan pip:pip install python-benedict

https://github.com/fabiocaccamo/python-benedict#flatten

from benedict import benedict 

d = benedict(data)
f = d.flatten(separator='_')
Fabio Caccamo
sumber