Reduzieren Sie verschachtelte Wörterbücher und komprimieren Sie die Schlüssel

Antworten:

218

Grundsätzlich müssen Sie auf die gleiche Weise, wie Sie eine verschachtelte Liste reduzieren würden, nur die zusätzliche Arbeit erledigen, um das Diktat nach Schlüssel / Wert zu iterieren, neue Schlüssel für Ihr neues Wörterbuch zu erstellen und das Wörterbuch im letzten Schritt zu erstellen.

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
quelle
7
Wenn Sie das isinstancedurch einen try..exceptBlock ersetzen , funktioniert dies für jede Zuordnung, auch wenn es nicht von abgeleitet ist dict.
Björn Pollex
1
Es wurde geändert, um es zu testen collections.MutableMapping, um es allgemeiner zu machen. Aber für Python <2.6 try..exceptist dies wahrscheinlich die beste Option.
Imran
5
Wenn Sie möchten, dass leere Wörterbücher in abgeflachter Version erhalten bleiben, können Sie if isinstance(v, collections.MutableMapping):zuif v and isinstance(v, collections.MutableMapping):
tarequeh
3
Beachten Sie, dass new_key = parent_key + sep + k if parent_key else kdavon ausgegangen wird, dass Schlüssel immer Zeichenfolgen sind, andernfalls wird sie ausgelöst TypeError: cannot concatenate 'str' and [other] objects. Sie können dies jedoch beheben, indem Sie einfach kzu string ( str(k)) zwingen oder Schlüssel zu einem Tupel anstelle eines Strings verketten (Tupel können auch Diktierschlüssel sein).
Scott H
1
Und die
Aufblasfunktion
65

Es gibt zwei wichtige Überlegungen, die das Originalplakat berücksichtigen muss:

  1. Gibt es Probleme mit dem Keyspace-Clobbering? Zum Beispiel {'a_b':{'c':1}, 'a':{'b_c':2}}würde ergeben {'a_b_c':???}. Die folgende Lösung umgeht das Problem, indem eine iterierbare Anzahl von Paaren zurückgegeben wird.
  2. Wenn die Leistung ein Problem darstellt, erfordert die Key-Reducer-Funktion (die ich hiermit als "Join" bezeichne) den Zugriff auf den gesamten Schlüsselpfad, oder kann O (1) einfach an jedem Knoten im Baum ausgeführt werden? Wenn Sie sagen wollen joinedKey = '_'.join(*keys), kostet Sie das O (N ^ 2) Laufzeit. Wenn Sie jedoch bereit sind zu sagen nextKey = previousKey+'_'+thisKey, erhalten Sie O (N) Zeit. Mit der folgenden Lösung können Sie beides tun (da Sie lediglich alle Schlüssel verketten und anschließend nachbearbeiten können).

(Leistung ist wahrscheinlich kein Problem, aber ich werde auf den zweiten Punkt eingehen, falls sich jemand anderes darum kümmert: Bei der Implementierung gibt es zahlreiche gefährliche Möglichkeiten. Wenn Sie dies rekursiv tun und nachgeben und wieder nachgeben oder etwas Äquivalentes, das berührt Knoten mehr als einmal (was aus Versehen recht einfach ist), erledigen Sie möglicherweise eher O (N ^ 2) als O (N). Dies liegt daran, dass Sie möglicherweise dann einen Schlüssel berechnen aund a_1dann a_1_i... und dann berechnen adann a_1dann a_1_ii..., aber eigentlich sollten Sie nicht noch a_1einmal rechnen müssen . Auch wenn Sie es nicht neu berechnen, ist es genauso schlecht, es erneut zu liefern (ein "Level-by-Level" -Ansatz). Ein gutes Beispiel ist über die Leistung nachdenken auf {1:{1:{1:{1:...(N times)...{1:SOME_LARGE_DICTIONARY_OF_SIZE_N}...}}}})

Unten ist eine Funktion, die ich geschrieben habe und flattenDict(d, join=..., lift=...)die an viele Zwecke angepasst werden kann und die Sie tun können, was Sie wollen. Leider ist es ziemlich schwierig, eine faule Version dieser Funktion zu erstellen, ohne die oben genannten Leistungseinbußen zu verursachen (viele Python-Buildins wie chain.from_iterable sind nicht wirklich effizient, was ich erst nach ausführlichen Tests von drei verschiedenen Versionen dieses Codes festgestellt habe, bevor ich mich entschieden habe dieses).

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

Um besser zu verstehen, was los ist, finden Sie unten ein Diagramm für diejenigen, die mit reduce(links) nicht vertraut sind , auch bekannt als "Links falten". Manchmal wird es mit einem Anfangswert anstelle von k0 gezeichnet (nicht Teil der Liste, an die Funktion übergeben). Hier Jist unsere joinFunktion. Wir verarbeiten jedes k n mit vor 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

Dies ist in der Tat dasselbe wie functools.reduce, aber wo unsere Funktion dies für alle Schlüsselpfade des Baums tut.

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

Demonstration (die ich sonst in docstring eingefügt hätte):

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

Performance:

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

... seufz, glaube nicht, dass einer meine Schuld ist ...


[unwichtiger historischer Hinweis aufgrund von Moderationsproblemen]

In Bezug auf das angebliche Duplikat von Flatten ein Wörterbuch von Wörterbüchern (2 Ebenen tief) von Listen in Python :

Die Lösung dieser Frage kann in Bezug auf diese Frage umgesetzt werden sorted( sum(flatten(...),[]) ). Das Gegenteil ist nicht möglich: Zwar können die Werte von flatten(...)aus dem angeblichen Duplikat durch Zuordnung eines Akkumulators höherer Ordnung wiederhergestellt werden, die Schlüssel können jedoch nicht wiederhergestellt werden. (Bearbeiten: Es stellt sich auch heraus, dass die Frage des mutmaßlichen Duplikatbesitzers völlig anders ist, da sie sich nur mit Wörterbüchern befasst, die genau 2 Ebenen tief sind, obwohl eine der Antworten auf dieser Seite eine allgemeine Lösung bietet.)

Ninjagecko
quelle
2
Ich bin mir nicht sicher, ob dies für die Frage relevant ist. Diese Lösung reduziert ein Wörterbuchelement einer Liste von Wörterbüchern nicht, dh {'a': [{'aa': 1}, {'ab': 2}]}. Die Funktion flattenDict kann leicht geändert werden, um diesem Fall Rechnung zu tragen.
Stewbaca
55

Oder wenn Sie bereits Pandas verwenden, können Sie dies folgendermaßen tun json_normalize():

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

Ausgabe:

{'a': 1, 'c_a': 2, 'c_b_x': 5, 'c_b_y': 10, 'd': [1, 2, 3]}
MYGz
quelle
4
oder einfach das sep Argument übergeben :)
Blue Moon
2
Etwas schade, dass es keine Listen behandelt :)
Roelant
31

Wenn Sie verwenden, pandasist in pandas.io.json._normalize1 eine Funktion versteckt , nested_to_recorddie genau dies tut.

from pandas.io.json._normalize import nested_to_record    

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

1 In Pandas-Versionen 0.24.xund älteren Anwendungen pandas.io.json.normalize(ohne die _)

Aaron N. Brock
quelle
1
Was für mich funktioniert hat war from pandas.io.json._normalize import nested_to_record. Beachten Sie den Unterstrich ( _) vor normalize.
Eyal Levin
2
@EyalLevin Guter Fang! Dies hat sich geändert 0.25.x, ich habe die Antwort aktualisiert. :)
Aaron N. Brock
28

Hier ist eine Art "funktionale", "einzeilige" Implementierung. Es ist rekursiv und basiert auf einem bedingten Ausdruck und einem diktierten Verständnis.

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 }

Prüfung:

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}
geteilt durch Null
quelle
Dies funktioniert nicht für allgemeine Wörterbücher, insbesondere mit Tupelschlüsseln, z. B. Ersatz ('hgf',2)für den 2. Schlüssel in Ihren TypeError
Testwürfen
@alancalvitti Dies setzt voraus, dass es sich um eine Zeichenfolge handelt oder um etwas anderes, das den +Operator unterstützt . Für alles andere müssen Sie sich prefix + separator + kan den entsprechenden Funktionsaufruf anpassen , um die Objekte zu erstellen.
Dividebyzero
Ein weiteres Problem, das für Tupelschlüssel relevant ist. Ich habe separat veröffentlicht, wie man basierend auf Ihrer Methode verallgemeinert. Es kann jedoch nicht richtig mit Ninjagekos Beispiel umgehen:{'a_b':{'c':1}, 'a':{'b_c':2}}
Alancalvitti
2
Ich machte mir Sorgen und sah keine Antworten mit Rekursion. Was ist heutzutage los mit unserer Jugend?
Jakov
tut nichts, wenn ein Diktat eine verschachtelte Liste von Diktaten hat, wie folgt:{'name': 'Steven', 'children': [{'name': 'Jessica', 'children': []}, {'name': 'George', 'children': []}]}
Gergely M
12

Code:

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

Ergebnisse:

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

Ich verwende Python3.2, Update für Ihre Version von Python.

Pavan Yalamanchili
quelle
Sie möchten wahrscheinlich den Standardwert von lkey=''in Ihrer Funktionsdefinition angeben, anstatt die Funktion aufzurufen. Siehe andere Antworten in dieser Hinsicht.
Acumenus
6

Wie wäre es mit einer funktionalen und performanten Lösung in 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(), 
        {}
    ))

Dies ist noch performanter:

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

In Benutzung:

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
quelle
2
Wie wäre es mit einer lesbaren und funktionierenden Lösung? ;) Auf welcher Version hast du das getestet? Beim Ausprobieren in Python 3.4.3 wird "Syntaxfehler" angezeigt. Die Verwendung von "** all" scheint nicht legitim zu sein.
Ingo Fischer
Ich arbeite seit Python 3.5. Wusste nicht, dass es mit 3.4 nicht funktioniert. Du hast recht, das ist nicht sehr lesbar. Ich habe die Antwort aktualisiert. Hoffe, es ist jetzt besser lesbar. :)
Rotareti
1
Fehlender Import reduzieren. Ich finde den Code immer noch schwer zu verstehen und ich denke, es ist ein gutes Beispiel, warum Guido van Rossum selbst bereits 2005 von der Verwendung von Lambda, Reduzieren, Filtern und Karten abgeraten hat: artima.com/weblogs/viewpost.jsp?thread=98196
Ingo Fischer
Genau. Python ist nicht wirklich für die funktionale Programmierung konzipiert . Trotzdem finde ich reducees großartig, wenn Sie Wörterbücher reduzieren müssen. Ich habe die Antwort aktualisiert. Sollte jetzt etwas pythonischer aussehen.
Rotareti
6

Dies ist nicht auf Wörterbücher beschränkt, sondern auf jeden Zuordnungstyp, der .items () implementiert. Weiter ist es schneller, da es eine if-Bedingung vermeidet. Trotzdem gehen die Credits an 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
quelle
1
Wenn des sich nicht um einen dictbenutzerdefinierten Zuordnungstyp handelt, der nicht implementiert wird items, schlägt Ihre Funktion sofort fehl. Es funktioniert also nicht für jeden Mapping-Typ, sondern nur für diejenigen, die implementiert werden items().
user6037143
@ user6037143 Haben Sie jemals einen Zuordnungstyp gefunden, der nicht implementiert ist items? Ich wäre neugierig, einen zu sehen.
Trey Hunner
1
@ user6037143, nein, Sie haben per Definition nicht, wenn Elemente nicht implementiert sind, ist es kein Zuordnungstyp.
Davoud Taghawi-Nejad
@ DavoudTaghawi-Nejad, könnten Sie dies ändern, um allgemeine Schlüssel zu verarbeiten, z. B. Tupel, die nicht intern abgeflacht werden sollten.
Alancalvitti
5

Meine Python 3.3-Lösung mit Generatoren:

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
quelle
Können Sie erweitern, um einen anderen gültigen Schlüsseltyp als str (einschließlich Tupel) zu verarbeiten? Fügen Sie ihnen anstelle der Verkettung von Zeichenfolgen ein Tupel hinzu.
Alancalvitti
4

Einfache Funktion zum Reduzieren verschachtelter Wörterbücher. Für Python 3, ersetzen .iteritems()mit.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

Die Idee / Anforderung war: Holen Sie sich flache Wörterbücher ohne übergeordnete Schlüssel.

Anwendungsbeispiel:

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}

Das Beibehalten von übergeordneten Schlüsseln ist ebenfalls einfach.

Efeu wächst
quelle
3

Dies ähnelt sowohl der Antwort von imran als auch von ralu. Es wird kein Generator verwendet, sondern eine Rekursion mit einem Abschluss verwendet:

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
quelle
Ich bin mir nicht sicher, ob die Verwendung des Begriffs " Abschluss " hier korrekt ist, da die Funktion _flatten_dictniemals zurückgegeben wird und auch nicht erwartet wird, dass sie jemals zurückgegeben wird. Es kann stattdessen möglicherweise als Unterfunktion oder als geschlossene Funktion bezeichnet werden.
Acumenus
3

Davouds Lösung ist sehr nett, liefert aber keine zufriedenstellenden Ergebnisse, wenn das verschachtelte Diktat auch Listen von Diktaten enthält, aber sein Code muss für diesen Fall angepasst werden:

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)
user3830731
quelle
Sie können das Ergebnis von zwischenspeichern type([]), um einen Funktionsaufruf für jedes Element der zu vermeiden dict.
Bfontaine
2
Bitte verwenden Sie isinstance(v, list)stattdessen
Druska
3

Rekursion nutzen, einfach und lesbar halten:

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

Anruf ist einfach:

new_dict = flatten_dict(dictionary)

oder

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

wenn wir das Standardtrennzeichen ändern wollen.

Eine kleine Aufschlüsselung:

Wenn die Funktion zum ersten Mal aufgerufen wird, wird sie nur mit der Übergabe aufgerufen, dictionarydie abgeflacht werden soll. Der accumulatorParameter dient hier zur Unterstützung der Rekursion, die wir später sehen werden. Wir instanziieren also accumulatorzu einem leeren Wörterbuch, in das wir alle verschachtelten Werte aus dem Original einfügen dictionary.

if accumulator is None:
    accumulator = {}

Während wir die Werte des Wörterbuchs durchlaufen, erstellen wir für jeden Wert einen Schlüssel. Das parent_keyArgument gilt Nonefür den ersten Aufruf, während es für jedes verschachtelte Wörterbuch den Schlüssel enthält, der darauf verweist. Daher stellen wir diesen Schlüssel voran.

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

Wenn der Wert, auf vden der Schlüssel kzeigt, ein Wörterbuch ist, ruft sich die Funktion selbst auf und übergibt das verschachtelte Wörterbuch, das accumulator(das als Referenz übergeben wird, sodass alle daran vorgenommenen Änderungen in derselben Instanz vorgenommen werden) und den Schlüssel, kdamit wir kann den verketteten Schlüssel erstellen. Beachten Sie die continueAussage. Wir möchten die nächste Zeile außerhalb des ifBlocks überspringen , damit das verschachtelte Wörterbuch nicht in der accumulatorUnter-Taste landet k.

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

Was tun wir also, wenn der Wert vkein Wörterbuch ist? Legen Sie es einfach unverändert in die accumulator.

accumulator[k] = v

Sobald wir fertig sind, geben wir einfach das zurück accumulatorund lassen das ursprüngliche dictionaryArgument unberührt.

HINWEIS

Dies funktioniert nur mit Wörterbüchern, die Zeichenfolgen als Schlüssel haben. Es funktioniert mit Hash-Objekten, die die __repr__Methode implementieren , führt jedoch zu unerwünschten Ergebnissen.

Jakov
quelle
2

Die obigen Antworten funktionieren wirklich gut. Ich dachte nur, ich würde die Unflatten-Funktion hinzufügen, die ich geschrieben habe:

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

Hinweis: Dies berücksichtigt nicht '_', das bereits in Schlüsseln vorhanden ist, ähnlich wie die abgeflachten Gegenstücke.

Tarequeh
quelle
2

Hier ist ein Algorithmus für den eleganten Austausch vor Ort. Getestet mit Python 2.7 und Python 3.5. Verwenden des Punktzeichens als Trennzeichen.

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

Beispiel:

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

Ausgabe:

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

Ich habe diesen Code hier zusammen mit der Matching- unflatten_jsonFunktion veröffentlicht.

Alexander Ryzhov
quelle
2

Wenn Sie ein flach verschachteltes Wörterbuch und eine Liste aller eindeutigen Schlüssel wünschen, ist hier die Lösung:

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
quelle
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
quelle
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
quelle
Dies funktioniert mit Listen in unserem verschachtelten Diktat, hat jedoch keine benutzerdefinierte Trennoption
Nikhil VJ
2

Ich dachte an eine Unterklasse von UserDict, um die Schlüssel automatisch zu plattieren.

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)

‌ Die Vorteile, dass Schlüssel ohne Überraschung im Handumdrehen oder mithilfe der Standard-Diktatinstanz hinzugefügt werden können:

>>> 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
quelle
1
Es ist ziemlich überraschend, fd ['person'] zuzuweisen, aber seinen vorhandenen Wert beizubehalten. So funktionieren reguläre Diktate nicht.
tbm
1

Generatoren verwenden:

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
quelle
2
type(i).__name__=='dict'könnte durch type(i) is dictoder vielleicht sogar besser isinstance(d, dict)(oder Mapping/ MutableMapping) ersetzt werden.
Cristian Ciupitu
1

Verwenden von dict.popitem () in einer einfachen Rekursion wie bei einer verschachtelten Liste:

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
quelle
1

Nicht genau das, was das OP verlangt hat, aber viele Leute kommen hierher, um nach Möglichkeiten zu suchen, verschachtelte JSON-Daten aus der realen Welt zu reduzieren, die verschachtelte Json-Objekte und Arrays mit Schlüsselwerten und Json-Objekte in den Arrays usw. enthalten können. JSON enthält keine Tupel, daher müssen wir uns darüber keine Sorgen machen.

Ich habe eine Implementierung des Kommentars zur Aufnahme von Listen durch @roneo auf die Antwort von @Imran gefunden :

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)

Probier es aus:

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}

Und das macht den Job, den ich machen muss: Ich werfe jeden komplizierten JSON darauf und es macht es für mich flacher.

Alle Credits an https://github.com/ScriptSmith .

Nikhil VJ
quelle
1

Ich habe kürzlich ein Paket namens Cherrypicker geschrieben, um genau diese Dinge zu erledigen, da ich es so oft tun musste!

Ich denke, der folgende Code würde Ihnen genau das geben, wonach Sie suchen:

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

Sie können das Paket installieren mit:

pip install cherrypicker

... und weitere Dokumente und Anleitungen finden Sie unter https://cherrypicker.readthedocs.io .

Andere Methoden sind möglicherweise schneller, aber die Priorität dieses Pakets besteht darin, solche Aufgaben zu vereinfachen . Wenn Sie jedoch eine große Liste von Objekten haben, die abgeflacht werden sollen, können Sie CherryPicker auch anweisen, die Parallelverarbeitung zu verwenden, um die Arbeit zu beschleunigen.

big-o
quelle
Ich mag den alternativen Ansatz.
Gergely M
0

Ich bevorzuge immer den Zugriff auf dictObjekte über .items(), daher verwende ich zum Reduzieren von Diktaten den folgenden rekursiven Generator flat_items(d). Wenn Sie es noch dicteinmal haben möchten, wickeln Sie es einfach so ein: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
quelle
0

Variation dieser verschachtelten Wörterbücher reduzieren , Schlüssel mit max_level und benutzerdefiniertem Reduzierer komprimieren .

  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)
user2528473
quelle
0

Wenn Ihnen rekursive Funktionen nichts ausmachen, finden Sie hier eine Lösung. Ich habe mir auch erlaubt, einen Ausschlussparameter einzuschließen, falls es einen oder mehrere Werte gibt, die Sie beibehalten möchten.

Code:

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

Verwendung:

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)

Ausgabe:

{'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
quelle
0

Ich habe einige der Lösungen auf dieser Seite ausprobiert - wenn auch nicht alle -, aber diejenigen, die ich ausprobiert habe, konnten die verschachtelte Liste von Diktaten nicht verarbeiten.

Betrachten Sie ein Diktat wie dieses:

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

Hier ist meine provisorische Lösung:

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

welches produziert:

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

Eine provisorische Lösung und nicht perfekt.
HINWEIS:

  • Es werden keine leeren Wörter wie das address: {}k / v-Paar gespeichert .

  • Dicts in verschachtelten Tupeln werden nicht abgeflacht - obwohl es einfach wäre, sie hinzuzufügen, wenn Python-Tupel ähnlich wie Listen funktionieren.

Gergely M.
quelle
-1

Verwenden python-benedictSie einfach , es ist eine Diktat-Unterklasse, die viele Funktionen bietet, einschließlich einer flattenMethode. Es ist möglich, es mit pip zu installieren:pip install python-benedict

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

from benedict import benedict 

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