Appiattire dizionari annidati, comprimere le chiavi


Risposte:


220

Fondamentalmente nello stesso modo in cui appiattiresti un elenco nidificato, devi solo fare il lavoro extra per iterare il dict per chiave / valore, creare nuove chiavi per il tuo nuovo dizionario e creare il dizionario nel passaggio finale.

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
Se lo sostituisci isinstancecon un try..exceptblocco, questo funzionerà per qualsiasi mappatura, anche se non è derivata dict.
Björn Pollex,

1
Modificato per testarlo per collections.MutableMappingrenderlo più generico. Ma per Python <2.6, try..exceptè probabilmente l'opzione migliore.
Imran,

5
Se vuoi che i dizionari vuoti if isinstance(v, collections.MutableMapping):if v and isinstance(v, collections.MutableMapping):
vengano

3
Si noti che new_key = parent_key + sep + k if parent_key else kpresuppone che le chiavi siano sempre stringhe, altrimenti aumenterà TypeError: cannot concatenate 'str' and [other] objects. Tuttavia, potresti risolverlo semplicemente copiando kstring ( str(k)) o concatenando chiavi in ​​una tupla anziché in una stringa (anche le tuple possono essere chiavi dict).
Scott H,

1
E la funzione di gonfiaggio è qui
mitch,

65

Ci sono due grandi considerazioni che il poster originale deve considerare:

  1. Ci sono problemi di clobbering nello spazio dei tasti? Per esempio,{'a_b':{'c':1}, 'a':{'b_c':2}} si tradurrebbe in {'a_b_c':???}. La soluzione seguente elude il problema restituendo un iterabile di coppie.
  2. Se le prestazioni sono un problema, la funzione di riduzione della chiave (che in seguito definisco 'join') richiede l'accesso all'intero percorso della chiave, oppure può semplicemente fare O (1) funzionare su tutti i nodi dell'albero? Se vuoi essere in grado di dire joinedKey = '_'.join(*keys), questo ti costerà O (N ^ 2) tempo di esecuzione. Tuttavia, se sei disposto a dirlo nextKey = previousKey+'_'+thisKey, ti farà guadagnare O (N) tempo. La soluzione seguente ti consente di fare entrambe le cose (poiché potresti semplicemente concatenare tutte le chiavi, quindi postprocessarle).

(Le prestazioni non sono probabilmente un problema, ma approfondirò il secondo punto nel caso in cui a chiunque interessi: Nell'attuare questo, ci sono numerose scelte pericolose. Se lo fai in modo ricorsivo e cedi e cedi, o qualcosa di equivalente che tocchi i nodi più di una volta (che è abbastanza facile da fare per caso), si stanno facendo potenzialmente O (2 ^ N) lavoro, piuttosto che O (N). Questo perché forse si sta calcolando una chiave apoi a_1poi a_1_i... e poi calcolando aallora a_1poi a_1_ii..., ma in realtà non dovresti dover calcolare di a_1nuovo. Anche se non lo stai ricalcolando, riproporlo (un approccio "livello per livello") è altrettanto negativo. Un buon esempio è pensare alla performance su {1:{1:{1:{1:...(N times)...{1:SOME_LARGE_DICTIONARY_OF_SIZE_N}...}}}})

Di seguito è una funzione che ho scritto flattenDict(d, join=..., lift=...)che può essere adattata a molti scopi e può fare quello che vuoi. Purtroppo è abbastanza difficile creare una versione pigra di questa funzione senza incorrere nelle penalità di prestazione sopra (molti builtin di Python come chain.from_iterable non sono effettivamente efficienti, che ho realizzato solo dopo test approfonditi di tre diverse versioni di questo codice prima di accontentarmi di questo).

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

Per capire meglio cosa sta succedendo, di seguito è riportato un diagramma per chi non ha familiarità con reduce(a sinistra), altrimenti noto come "piega a sinistra". A volte viene disegnato con un valore iniziale al posto di k0 (non parte dell'elenco, passato nella funzione). Ecco la Jnostra joinfunzione. Preelaboriamo ogni k n con 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

Questo è in realtà lo stesso di functools.reduce, ma dove la nostra funzione fa questo a tutti i percorsi chiave dell'albero.

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

Dimostrazione (che altrimenti metterei in 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}

Prestazione:

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

... sospiro, non pensare che sia colpa mia ...


[nota storica non importante a causa di problemi di moderazione]

Per quanto riguarda il presunto duplicato di Flatten, un dizionario di dizionari (2 livelli di profondità) di elenchi in Python :

La soluzione di quella domanda può essere implementata in termini di questa facendo sorted( sum(flatten(...),[]) ). Il contrario non è possibile: mentre è vero che i valori di flatten(...)possono essere recuperati dal presunto duplicato mappando un accumulatore di ordine superiore, non è possibile recuperare le chiavi. (modifica: Inoltre si scopre che la presunta domanda del proprietario duplicato è completamente diversa, in quanto tratta solo dizionari con una profondità esattamente di 2 livelli, sebbene una delle risposte in quella pagina fornisca una soluzione generale.)


2
Non sono sicuro che ciò sia pertinente alla domanda. Questa soluzione non appiattisce un elemento del dizionario di un elenco di dizionari, ovvero {'a': [{'aa': 1}, {'ab': 2}]}. La funzione flattenDict può essere modificata facilmente per adattarsi a questo caso.
Stewbaca,

55

O se stai già usando i panda, puoi farlo in questo json_normalize()modo:

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

Produzione:

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

4
o semplicemente passa l'argomento sep :)
Blue Moon,

2
Peccato che non gestisca gli elenchi :)
Roelant,

31

Se stai usando pandasc'è una funzione nascosta in pandas.io.json._normalize1 chiamata nested_to_recordche fa esattamente questo.

from pandas.io.json._normalize import nested_to_record    

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

1 Nelle versioni panda 0.24.xe precedente pandas.io.json.normalize(senza _)


1
Ciò che ha funzionato per me è stato from pandas.io.json._normalize import nested_to_record. Notare il trattino basso ( _) prima normalize.
Eyal Levin,

2
@EyalLevin Buona cattura! Questo è cambiato in 0.25.x, ho aggiornato la risposta. :)
Aaron N. Brock,

28

Ecco una sorta di implementazione "funzionale", "one-liner". È ricorsivo e basato su un'espressione condizionale e una comprensione dettata.

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}

Questo non funziona per i dizionari generali, in particolare, con i tasti tupla, ad esempio sostituendo ('hgf',2)il 2 ° tasto nei test TypeError
lanciati

@alancalvitti Questo presuppone che sia una stringa o qualcos'altro che supporta l' +operatore. Per qualsiasi altra cosa dovrai adattarti prefix + separator + kalla chiamata della funzione appropriata per comporre gli oggetti.
dividebyzero,

Un altro problema relativo alle chiavi tuple. Ho pubblicato separatamente come generalizzare in base al tuo metodo. Tuttavia non può gestire correttamente l'esempio di ninjageko:{'a_b':{'c':1}, 'a':{'b_c':2}}
alancalvitti,

2
Mi stavo preoccupando, non vedevo risposte utilizzando la ricorsione. Cosa c'è di sbagliato nella nostra giovinezza in questi giorni?
Jakov,

non fa nulla se un dict ha un elenco annidato di dadi, in questo modo:{'name': 'Steven', 'children': [{'name': 'Jessica', 'children': []}, {'name': 'George', 'children': []}]}
Gergely M

12

Codice:

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

risultati:

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

Sto usando python3.2, aggiornamento per la tua versione di Python.


Probabilmente si desidera specificare il valore predefinito di lkey=''nella definizione della funzione anziché quando si chiama la funzione. Vedi altre risposte al riguardo.
Acumenus,

6

Che ne dici di una soluzione funzionale e performante 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(), 
        {}
    ))

Questo è ancora più performante:

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 uso:

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
Che ne dici di una soluzione leggibile e funzionante? ;) Su quale versione hai provato questo? Ricevo "Errore di sintassi" quando provo questo in Python 3.4.3. Sembra che l'uso di "** all" non sia legittimo.
Ingo Fischer,

Lavoro da Python 3.5. Non sapevo che non funzionasse con 3.4. Hai ragione, questo non è molto leggibile. Ho aggiornato la risposta. Spero sia più leggibile ora. :)
Rotareti,

1
Aggiunto mancante riduzione dell'importazione. Trovo ancora difficile capire il codice e penso che sia un buon esempio del perché Guido van Rossum stesso abbia già scoraggiato l'uso di lambda, riduzione, filtro e mappa nel 2005: artima.com/weblogs/viewpost.jsp?thread=98196
Ingo Fischer

Sono d'accordo. Python non è progettato per la programmazione funzionale . Penso comunque che reducesia fantastico nel caso in cui sia necessario ridurre i dizionari. Ho aggiornato la risposta. Ora dovrebbe apparire un po 'più pitonico.
Rotareti,

6

Questo non è limitato ai dizionari, ma a ogni tipo di mappatura che implementa .items (). Inoltre è più veloce in quanto evita una condizione if. Tuttavia i crediti vanno a 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)

1
Se dnon è un dicttipo di mappatura personalizzato ma non implementato items, la tua funzione fallirebbe subito. Quindi, non funziona per ogni tipo di mappatura ma solo per quelli che implementano items().
user6037143

@ user6037143 hai mai riscontrato un tipo di mappatura che non implementa items? Sarei curioso di vederne uno.
Trey Hunner,

1
@ user6037143, no non lo hai per definizione se gli articoli non sono implementati non è un tipo di mappatura.
Davoud Taghawi-Nejad,

@ DavoudTaghawi-Nejad, potresti modificarlo per gestire chiavi generali, ad esempio tuple che non dovrebbero essere appiattite internamente.
alancalvitti,

5

La mia soluzione Python 3.3 usando generatori:

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}

puoi estendere per gestire qualsiasi tipo di chiave valido diverso da str (compresa la tupla)? Invece di concatenare le stringhe, unisciti a loro in una tupla.
alancalvitti,

4

Semplice funzione per appiattire i dizionari nidificati. Per Python 3, sostituirlo .iteritems()con.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

L'idea / requisito era: ottenere dizionari piatti senza conservare le chiavi parent.

Esempio di utilizzo:

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}

Anche mantenere le chiavi dei genitori è semplice.


4

Utilizzando la ricorsione, rendendola semplice e leggibile dall'uomo:

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

La chiamata è semplice:

new_dict = flatten_dict(dictionary)

o

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

se vogliamo cambiare il separatore predefinito.

Un piccolo guasto:

Quando la funzione viene chiamata per la prima volta, viene chiamata solo passando il dictionaryche vogliamo appiattire. Il accumulatorparametro è qui per supportare la ricorsione, che vedremo più avanti. Quindi, creiamo un'istanza accumulatorin un dizionario vuoto in cui inseriremo tutti i valori nidificati dall'originale dictionary.

if accumulator is None:
    accumulator = {}

Mentre ripetiamo i valori del dizionario, costruiamo una chiave per ogni valore. L' parent_keyargomento sarà Noneper la prima chiamata, mentre per ogni dizionario nidificato, conterrà la chiave che punta ad essa, quindi anteponiamo quella chiave.

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

Nel caso in cui il valore a cui punta vla chiave ksia un dizionario, la funzione chiama se stessa, passando il dizionario nidificato, il accumulator(che viene passato per riferimento, quindi tutte le modifiche apportate ad esso vengono eseguite nella stessa istanza) e la chiave in kmodo che può costruire la chiave concatenata. Si noti la continuedichiarazione. Vogliamo saltare la riga successiva, al di fuori del ifblocco, in modo che il dizionario nidificato non finisca nella accumulatorchiave sotto k.

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

Quindi, cosa facciamo nel caso in cui il valore vnon sia un dizionario? Mettilo invariato all'interno del file accumulator.

accumulator[k] = v

Una volta terminato, restituiamo semplicemente il accumulator, lasciando dictionaryintatta l'argomento originale .

NOTA

Funzionerà solo con dizionari che hanno stringhe come chiavi. Funzionerà con oggetti hash che implementano il __repr__metodo, ma produrrà risultati indesiderati.


3

Questo è simile alla risposta di Imran e di Ralu. Non utilizza un generatore, ma utilizza invece la ricorsione con una chiusura:

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}

Non sono sicuro se l'utilizzo del termine " chiusura " sia corretto qui, poiché la funzione _flatten_dictnon viene mai restituita, né si prevede che venga mai restituita. Può invece essere definita come una sottofunzione o una funzione chiusa .
Acumenus,

3

La soluzione di Davoud è molto bella ma non dà risultati soddisfacenti quando il dict nidificato contiene anche elenchi di dadi, ma il suo codice può essere adattato per quel caso:

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)

È possibile memorizzare nella cache il risultato di type([])per evitare una chiamata di funzione per ogni elemento di dict.
bfontaine,

2
Si prega di utilizzare isinstance(v, list)invece
Druska il

2

Le risposte sopra funzionano davvero bene. Ho pensato di aggiungere la funzione non appiattita che ho scritto:

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

Nota: questo non tiene conto di "_" già presente nelle chiavi, proprio come le controparti piatte.


2

Ecco un algoritmo per una sostituzione elegante e diretta. Testato con Python 2.7 e Python 3.5. Utilizzando il carattere punto come separatore.

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

Esempio:

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

Produzione:

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

Ho pubblicato questo codice qui insieme alla unflatten_jsonfunzione corrispondente .


2

Se vuoi un dizionario nidificato piatto e desideri un elenco di chiavi univoci, ecco la soluzione:

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

funziona con le liste all'interno del nostro dict nidificato, ma non ha un'opzione di separazione personalizzata
Nikhil VJ

2

Stavo pensando a una sottoclasse di UserDict per appiattire automagicamente le chiavi.

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)

‌ I vantaggi che le chiavi possono essere aggiunte al volo, o usando l'installazione standard di dict, senza sorpresa:

>>> 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
Assegnare a fd ['persona'] ma mantenere il suo valore esistente è abbastanza sorprendente. Non è così che funzionano i dadi regolari.
martedì

1

Utilizzando generatori:

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'potrebbe essere sostituito con type(i) is dicto forse anche meglio isinstance(d, dict)(o Mapping/ MutableMapping).
Cristian Ciupitu,

1

Utilizzo di dict.popitem () nella semplice ricorsione simile a una lista nidificata:

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

Non esattamente quello che l'OP ha chiesto, ma molte persone stanno venendo qui alla ricerca di modi per appiattire i dati JSON nidificati nel mondo reale che possono avere oggetti json e array chiave-valore nidificati all'interno degli array e così via. JSON non include le tuple, quindi non dobbiamo preoccuparci di quelle.

Ho trovato un'implementazione del commento di inclusione dell'elenco di @roneo alla risposta pubblicata da @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)

Provalo:

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 fa il lavoro che devo fare: lancio qualsiasi complicato json e questo lo appiattisce per me.

Tutti i crediti su https://github.com/ScriptSmith .


1

Di recente ho scritto un pacchetto chiamato cherrypicker per occuparmi di questo esatto genere di cose, dal momento che dovevo farlo così spesso!

Penso che il seguente codice ti darebbe esattamente quello che stai cercando:

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

Puoi installare il pacchetto con:

pip install cherrypicker

... e ci sono più documenti e indicazioni su https://cherrypicker.readthedocs.io .

Altri metodi possono essere più veloce, ma la priorità di questo pacchetto è quello di rendere tali compiti facili . Se hai un grande elenco di oggetti da appiattire, puoi anche dire a CherryPicker di usare l'elaborazione parallela per accelerare le cose.


Mi piace l'approccio alternativo.
Gergely M

0

Preferisco sempre gli dictoggetti di accesso tramite .items(), quindi per i dadi appiattiti uso il seguente generatore ricorsivo flat_items(d). Se ti piace averlo di dictnuovo, semplicemente avvolgilo in questo modo: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

Variazione dei dizionari nidificati di Flatten, compressione delle chiavi con max_level e riduttore personalizzato.

  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

Se non ti dispiace le funzioni ricorsive, ecco una soluzione. Ho anche preso la libertà di includere un'esclusione parametro di nel caso in cui ci siano uno o più valori che desideri mantenere.

Codice:

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

Uso:

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)

Produzione:

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

Ho provato alcune delle soluzioni in questa pagina - sebbene non tutte - ma quelle che ho provato non sono riuscite a gestire l'elenco annidato di dict.

Prendi in considerazione un detto come questo:

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

Ecco la mia soluzione improvvisata:

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

che produce:

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

Una soluzione improvvisata e non è perfetta.
NOTA:

  • non mantiene vuoti come la address: {}coppia k / v.

  • non appiattirà i cubetti nelle tuple nidificate, anche se sarebbe facile aggiungerlo considerando il fatto che le tuple di pitone si comportano in modo simile agli elenchi.


Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.