Spłaszcz zagnieżdżone słowniki, kompresja kluczy


Odpowiedzi:


220

Zasadniczo w ten sam sposób, w jaki spłaszczyłbyś zagnieżdżoną listę, musisz po prostu wykonać dodatkową pracę, aby iterować dyktafon według klucza / wartości, tworząc nowe klucze dla nowego słownika i tworząc słownik w ostatnim kroku.

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}

7
Jeśli zastąpić isinstancez try..exceptbloku, to będzie pracować dla dowolnego odwzorowania, nawet jeśli nie jest pochodną dict.
Björn Pollex

1
Zmieniono go do testowania, collections.MutableMappingaby był bardziej ogólny. Ale dla Pythona <2.6 try..exceptjest to prawdopodobnie najlepsza opcja.
Imran

5
Jeśli chcesz zachować puste słowniki w spłaszczonej wersji, możesz zmienić if isinstance(v, collections.MutableMapping):naif v and isinstance(v, collections.MutableMapping):
tarequeh.

3
Zauważ, że new_key = parent_key + sep + k if parent_key else kzakłada się, że klucze są zawsze łańcuchami, w przeciwnym razie wzrosną TypeError: cannot concatenate 'str' and [other] objects. Jednak możesz to naprawić, po prostu wymuszając metodę kstring ( str(k)) lub łącząc klucze w krotkę zamiast w łańcuch (krotki również mogą być kluczami dict).
Scott H

1
A funkcja nadmuchu jest tutaj
mitch

65

Istnieją dwie ważne kwestie, które należy wziąć pod uwagę przy oryginalnym plakacie:

  1. Czy występują problemy z blokowaniem przestrzeni klawiszy? Na przykład {'a_b':{'c':1}, 'a':{'b_c':2}}spowoduje to {'a_b_c':???}. Poniższe rozwiązanie pozwala uniknąć problemu, zwracając iterowalne pary.
  2. Jeśli wydajność jest problemem, czy funkcja redukcji klucza (którą tutaj nazywam „złączeniem”) wymaga dostępu do całej ścieżki klucza, czy może po prostu wykonać O (1) działanie w każdym węźle w drzewie? Jeśli chcesz móc powiedzieć joinedKey = '_'.join(*keys), będzie cię to kosztować O (N ^ 2) czasu pracy. Jeśli jednak chcesz to powiedzieć nextKey = previousKey+'_'+thisKey, masz czas O (N). Poniższe rozwiązanie pozwala ci zrobić jedno i drugie (ponieważ możesz po prostu połączyć wszystkie klucze, a następnie przetworzyć je ponownie).

(Wydajność nie jest prawdopodobnie problemem, ale omówię drugi punkt na wypadek, gdyby ktokolwiek inny się tym przejmował: wdrażając to, istnieje wiele niebezpiecznych wyborów. Jeśli robisz to rekurencyjnie i dajesz i ponownie dajesz, lub cokolwiek równoważnego, co dotyka węzłów więcej niż raz (co jest dość łatwe do przypadkowego zrobienia), potencjalnie wykonujesz pracę O (N ^ 2) zamiast O (N). To dlatego, że być może obliczasz klucz, aa a_1potem a_1_i..., a potem obliczasz anastępnie a_1następnie a_1_ii..., ale tak naprawdę nie powinno mieć obliczyć a_1ponownie. Nawet jeśli nie są przeliczania go ponownie otrzymując go (podejście „poziom po poziomie”) jest tak źle. dobrym przykładem jest myśleć o występie na {1:{1:{1:{1:...(N times)...{1:SOME_LARGE_DICTIONARY_OF_SIZE_N}...}}}})

Poniżej znajduje się funkcja, którą napisałem, flattenDict(d, join=..., lift=...)która może być dostosowana do wielu celów i może robić, co chcesz. Niestety, dość trudno jest stworzyć leniwą wersję tej funkcji bez ponoszenia powyższych kar za wydajność (wiele wbudowanych funkcji Pythona, takich jak chain.from_iterable, nie jest w rzeczywistości wydajnych, co zdałem sobie sprawę dopiero po szeroko zakrojonych testach trzech różnych wersji tego kodu, zanim zdecydowałem się na ten).

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

Aby lepiej zrozumieć, co się dzieje, poniżej znajduje się diagram dla osób niezaznajomionych z reduce(po lewej), znanym również jako „złóż w lewo”. Czasami jest rysowany z wartością początkową zamiast k0 (nie jest częścią listy, przekazywana do funkcji). Oto Jnasza joinfunkcja. Przetwarzamy wstępnie każdy k n z 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

W rzeczywistości jest to to samo, co functools.reduce, ale gdzie nasza funkcja robi to ze wszystkimi kluczowymi ścieżkami drzewa.

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

Demonstracja (którą w innym przypadku umieściłbym w dokumentacji):

>>> 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}

Występ:

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

... westchnij, nie myśl, że to moja wina ...


[nieważna notatka historyczna z powodu problemów z moderacją]

Odnośnie domniemanego duplikatu Flatten słownika słowników (głęboki na 2 poziomy) list w Pythonie :

Rozwiązanie tego pytania można zaimplementować w ramach tego przez działanie sorted( sum(flatten(...),[]) ). Odwrotna nie jest możliwe: ile jest prawdą, że wartość z flatten(...)może być odzyskany z domniemanym podwójnie przez akumulator mapowania wyższego rzędu, to nie można odzyskać kluczy. (edytuj: Okazuje się również, że pytanie domniemanego powielonego właściciela jest zupełnie inne, ponieważ dotyczy tylko słowników o głębokości dokładnie 2 poziomów, chociaż jedna z odpowiedzi na tej stronie daje ogólne rozwiązanie.)


2
Nie jestem pewien, czy jest to istotne dla pytania. To rozwiązanie nie spłaszcza pozycji słownika na liście słowników, tj. {'A': [{'aa': 1}, {'ab': 2}]}. Funkcję flattenDict można łatwo zmienić, aby dostosować ją do tego przypadku.
Stewbaca

55

A jeśli już używasz pand, możesz to zrobić w następujący json_normalize()sposób:

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])

Wynik:

{'a': 1, 'c_a': 2, 'c_b_x': 5, 'c_b_y': 10, 'd': [1, 2, 3]}

4
lub po prostu przekaż argument sep :)
Blue Moon

2
Szkoda, że ​​nie obsługuje list :)
Roelant

31

Jeśli używasz, pandasistnieje funkcja ukryta w pandas.io.json._normalize1 o nazwie, nested_to_recordktóra robi to dokładnie.

from pandas.io.json._normalize import nested_to_record    

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

1 W wersjach pandy 0.24.xi starszym użyciu pandas.io.json.normalize(bez _)


1
Dla mnie zadziałało from pandas.io.json._normalize import nested_to_record. Zwróć uwagę na podkreślenie ( _) przed normalize.
Eyal Levin

2
@EyalLevin Dobry chwyt! Zmieniło się to 0.25.x, zaktualizowałem odpowiedź. :)
Aaron N. Brock

28

Oto rodzaj „funkcjonalnej”, „jednowierszowej” implementacji. Jest rekurencyjny i oparty na wyrażeniu warunkowym i dyktowaniu.

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 }

Test:

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}

To nie działa w przypadku słowników ogólnych, w szczególności z kluczami krotki, np. Substytut ('hgf',2)2. klucza w twoich rzutach testowychTypeError
alancalvitti

@alancalvitti Zakłada się, że jest to ciąg znaków lub coś innego, co obsługuje +operator. W przypadku czegokolwiek innego będziesz musiał dostosować prefix + separator + księ do odpowiedniego wywołania funkcji, aby skomponować obiekty.
dividebyzero

Kolejna kwestia dotycząca kluczy krotek. Osobno opublikowałem, jak uogólniać na podstawie twojej metody. Jednak nie radzi sobie poprawnie z przykładem {'a_b':{'c':1}, 'a':{'b_c':2}}
ninjageko

2
Martwiłem się, nie widząc odpowiedzi wykorzystujących rekurencję. Co jest nie tak z naszą młodością w dzisiejszych czasach?
Jakov

nie robi nic, jeśli dyktando zawiera zagnieżdżoną listę dykt, na przykład:{'name': 'Steven', 'children': [{'name': 'Jessica', 'children': []}, {'name': 'George', 'children': []}]}
Gergely M

12

Kod:

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,''))

Wyniki:

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

Używam python3.2, aktualizacja dla twojej wersji pythona.


Prawdopodobnie chcesz określić domyślną wartość lkey=''w definicji funkcji zamiast podczas wywoływania funkcji. Zobacz inne odpowiedzi w tym zakresie.
Acumenus

6

Co powiesz na funkcjonalne i wydajne rozwiązanie w Pythonie 3.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(), 
        {}
    ))

To jest jeszcze bardziej wydajne:

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(), 
        {}
    ))

W użyciu:

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}

2
Co powiesz na czytelne i działające rozwiązanie? ;) Na której wersji to testowałeś? Podczas wypróbowywania tego w Pythonie 3.4.3 pojawia się komunikat „Błąd składni”. Wygląda na to, że użycie „** all” nie jest legalne.
Ingo Fischer

Pracuję od Pythona 3.5. Nie wiedziałem, że to nie działa z 3.4. Masz rację, to nie jest zbyt czytelne. Zaktualizowałem odpowiedź. Mam nadzieję, że teraz jest bardziej czytelny. :)
Rotareti

1
Dodano brakujący import redukcji. Wciąż wydaje mi się, że kod jest trudny do zrozumienia i myślę, że jest to dobry przykład, dlaczego sam Guido van Rossum odradzał używanie lambdy, redukcji, filtrowania i mapowania już w 2005 roku: artima.com/weblogs/viewpost.jsp?thread=98196
Ingo Fischer

Zgadzam się. Python nie jest tak naprawdę zaprojektowany do programowania funkcjonalnego . Mimo to uważam, że reducejest to świetne rozwiązanie, jeśli trzeba zmniejszyć liczbę słowników. Zaktualizowałem odpowiedź. Powinien wyglądać teraz trochę bardziej pytonicznie.
Rotareti

6

Nie jest to ograniczone do słowników, ale do każdego typu odwzorowania, który implementuje .items (). Dalej jest szybszy, ponieważ unika warunku „jeśli”. Niemniej jednak kredyty trafiają do Imrana:

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)

1
Jeśli dnie jest dictniestandardowym typem mapowania, który nie jest implementowany items, Twoja funkcja od razu zakończy się niepowodzeniem. Więc to nie działa dla każdego typu mapowania, ale tylko te, które implementują items().
user6037143

@ user6037143 Czy kiedykolwiek napotkałeś typ mapowania, który nie jest zaimplementowany items? Byłbym ciekawy, żeby taki zobaczyć.
Trey Hunner

1
@ user6037143, nie, nie masz tego z definicji, jeśli elementy nie są zaimplementowane, to nie jest typ mapowania.
Davoud Taghawi-Nejad

@ DavoudTaghawi-Nejad, czy możesz to zmodyfikować, aby obsługiwać klucze ogólne, np. Krotki, które nie powinny być wewnętrznie spłaszczane.
alancalvitti

5

Moje rozwiązanie dla Pythona 3.3 przy użyciu generatorów:

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}

czy możesz rozszerzyć, aby obsłużyć dowolny prawidłowy typ klucza inny niż str (w tym krotka)? Zamiast konkatenacji ciągów, połącz je w krotkę.
alancalvitti

4

Prosta funkcja spłaszczania zagnieżdżonych słowników. Pythona 3, zastępuje .iteritems()się.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

Pomysł / wymóg brzmiał: Uzyskaj płaskie słowniki bez przechowywania kluczy nadrzędnych.

Przykład użycia:

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}

Przechowywanie kluczy rodzica również jest proste.


4

Wykorzystując rekursję, zachowując prostotę i czytelność dla człowieka:

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

Połączenie jest proste:

new_dict = flatten_dict(dictionary)

lub

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

jeśli chcemy zmienić domyślny separator.

Mały podział:

Kiedy funkcja jest wywoływana po raz pierwszy, jest wywoływana tylko przekazując to, dictionaryco chcemy spłaszczyć. accumulatorParametrem jest tutaj do rekursji wsparcia, które zobaczymy później. Więc tworzymy instancję accumulatorw pustym słowniku, w którym umieścimy wszystkie zagnieżdżone wartości z oryginału dictionary.

if accumulator is None:
    accumulator = {}

Podczas iteracji po wartościach słownika tworzymy klucz dla każdej wartości. parent_keyArgumentem będzie Nonedo pierwszego połączenia, podczas gdy dla każdego zagnieżdżonego słownika, będzie zawierać klucz wskazując na niego, więc poprzedzić ten klucz.

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

W przypadku, gdy wartość wskazywana vprzez klucz kjest słownikiem, funkcja wywołuje samą siebie, przekazując zagnieżdżony słownik, accumulator(który jest przekazywany przez odniesienie, więc wszystkie wprowadzone w nim zmiany są dokonywane w tej samej instancji) i klucz k, abyśmy może skonstruować połączony klucz. Zwróć uwagę na continueoświadczenie. Chcemy pominąć następną linię, znajdującą się poza ifblokiem, aby zagnieżdżony słownik nie znalazł się w accumulatorkluczu under k.

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

Co więc zrobimy, jeśli wartość vnie jest słownikiem? Po prostu włóż go bez zmian do accumulator.

accumulator[k] = v

Kiedy skończymy, po prostu zwracamy accumulator, pozostawiając oryginalny dictionaryargument nietknięty.

UWAGA

Działa to tylko ze słownikami, które mają ciągi znaków jako klucze. Będzie działać z obiektami, które można mieszać, implementując tę __repr__metodę, ale przyniesie niepożądane wyniki.


3

Jest to podobne do odpowiedzi imrana i ralu. Nie używa generatora, ale zamiast tego wykorzystuje rekursję z zamknięciem:

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}

Nie jestem pewien, czy użycie terminu „ zamknięcie ” jest tutaj poprawne, ponieważ funkcja _flatten_dictnigdy nie jest zwracana ani nie oczekuje się jej zwrotu. Zamiast tego może być nazywana podfunkcją lub funkcją zamkniętą .
Acumenus

3

Rozwiązanie Davouda jest bardzo ładne, ale nie daje zadowalających wyników, gdy zagnieżdżony dykt zawiera również listy dykt, ale jego kod jest dostosowany do tego przypadku:

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)

Możesz buforować wynik polecenia, type([])aby uniknąć wywołania funkcji dla każdego elementu dict.
bfontaine

2
Proszę użyć isinstance(v, list)zamiast tego
Druska

2

Powyższe odpowiedzi działają naprawdę dobrze. Pomyślałem, że dodam niespłaszczoną funkcję, którą napisałem:

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

Uwaga: to nie uwzględnia znaku „_” już obecnego w kluczach, podobnie jak ich odpowiedniki spłaszczone.


2

Oto algorytm eleganckiej wymiany na miejscu. Testowane w Pythonie 2.7 i Pythonie 3.5. Używanie kropki jako separatora.

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

Przykład:

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

Wynik:

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

Opublikowałem ten kod tutaj wraz z unflatten_jsonfunkcją dopasowania .


2

Jeśli chcesz spłaszczyć zagnieżdżony słownik i chcesz mieć listę wszystkich unikalnych kluczy, oto rozwiązanie:

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))

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

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

działa to z listami wewnątrz naszego zagnieżdżonego dyktowania, ale nie ma niestandardowej opcji separatora
Nikhil VJ

2

Myślałem o podklasie UserDict do automagicznego spłaszczania klawiszy.

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)

‌ Zalety polegające na tym, że klucze można dodawać w locie lub przy użyciu standardowej instrukcji dyktowania, bez zaskoczenia:

>>> 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'}

1
Przypisanie fd [„osoba”] przy zachowaniu dotychczasowej wartości jest dość zaskakujące. Nie tak działają zwykłe dykty.
tbm

1

Korzystanie z generatorów:

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}

2
type(i).__name__=='dict'można by zastąpić type(i) is dictlub nawet lepiej isinstance(d, dict)(lub Mapping/ MutableMapping).
Cristian Ciupitu

1

Używanie dict.popitem () w prostej rekursji podobnej do zagnieżdżonej listy:

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)}

1

Nie jest to dokładnie to, o co prosił OP, ale wielu ludzi przychodzi tutaj, szukając sposobów na spłaszczenie zagnieżdżonych danych JSON w świecie rzeczywistym, które mogą mieć zagnieżdżone obiekty json i tablice klucz-wartość oraz obiekty JSON wewnątrz tablic i tak dalej. JSON nie zawiera krotek, więc nie musimy się tym martwić.

Znalazłem implementację komentarza włączenia listy autorstwa @roneo do odpowiedzi zamieszczonej przez @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)

Sprawdź to:

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}

I to robi to, czego potrzebuję: rzucam na to skomplikowany plik json, a to dla mnie wyrównuje.

Wszystkie kredyty do https://github.com/ScriptSmith .


1

Właściwie napisałem niedawno pakiet o nazwie cherrypicker, aby poradzić sobie z tego rodzaju rzeczami, ponieważ musiałem to robić tak często!

Myślę, że poniższy kod dałby ci dokładnie to, czego szukasz:

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()

Możesz zainstalować pakiet za pomocą:

pip install cherrypicker

... a więcej dokumentów i wskazówek znajdziesz na https://cherrypicker.readthedocs.io .

Inne metody mogą być szybsze, ale priorytetem tego pakietu jest, aby takie zadania łatwe . Jeśli jednak masz dużą listę obiektów do spłaszczenia, możesz również powiedzieć CherryPicker, aby używał przetwarzania równoległego w celu przyspieszenia działania.


Podoba mi się alternatywne podejście.
Gergely M

0

Zawsze wolę uzyskiwać dostęp do dictobiektów przez .items(), więc do spłaszczania dykt używam następującego generatora rekurencyjnego flat_items(d). Jeśli chcesz mieć dictponownie, po prostu zawiń to w ten sposób: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

0

Odmiana tych zagnieżdżonych słowników Flatten, kompresja kluczy z maksymalnym poziomem i niestandardową redukcją.

  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)

0

Jeśli nie masz nic przeciwko funkcjom rekurencyjnym, oto rozwiązanie. Pozwoliłem sobie również na uwzględnienie parametru wykluczenia na wypadek, gdyby istniała jedna lub więcej wartości, które chcesz zachować.

Kod:

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

Stosowanie:

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)

Wynik:

{'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}}

0

Wypróbowałem niektóre rozwiązania na tej stronie - choć nie wszystkie - ale te, które próbowałem, nie poradziły sobie z zagnieżdżoną listą dykt.

Rozważ taki dykt:

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': []
                 }
            ]
        }
    }

Oto moje prowizoryczne rozwiązanie:

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

który produkuje:

{
  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,
}

Prowizoryczne rozwiązanie i nie jest doskonałe.
UWAGA:

  • nie zachowuje pustych dykt, takich jak address: {}para k / v.

  • nie spłaszczy dykt w zagnieżdżonych krotkach - chociaż łatwo byłoby to dodać, wykorzystując fakt, że krotki Pythona działają podobnie do list.


Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.