Supponiamo di avere un dizionario come:
{'a': 1,
'c': {'a': 2,
'b': {'x': 5,
'y' : 10}},
'd': [1, 2, 3]}
Come potresti appiattirlo in qualcosa del tipo:
{'a': 1,
'c_a': 2,
'c_b_x': 5,
'c_b_y': 10,
'd': [1, 2, 3]}
Supponiamo di avere un dizionario come:
{'a': 1,
'c': {'a': 2,
'b': {'x': 5,
'y' : 10}},
'd': [1, 2, 3]}
Come potresti appiattirlo in qualcosa del tipo:
{'a': 1,
'c_a': 2,
'c_b_x': 5,
'c_b_y': 10,
'd': [1, 2, 3]}
Risposte:
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}
isinstance
con un try..except
blocco, questo funzionerà per qualsiasi mappatura, anche se non è derivata dict
.
collections.MutableMapping
renderlo più generico. Ma per Python <2.6, try..except
è probabilmente l'opzione migliore.
if isinstance(v, collections.MutableMapping):
if v and isinstance(v, collections.MutableMapping):
new_key = parent_key + sep + k if parent_key else k
presuppone che le chiavi siano sempre stringhe, altrimenti aumenterà TypeError: cannot concatenate 'str' and [other] objects
. Tuttavia, potresti risolverlo semplicemente copiando k
string ( str(k)
) o concatenando chiavi in una tupla anziché in una stringa (anche le tuple possono essere chiavi dict).
Ci sono due grandi considerazioni che il poster originale deve considerare:
{'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.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 a
poi a_1
poi a_1_i
... e poi calcolando a
allora a_1
poi a_1_ii
..., ma in realtà non dovresti dover calcolare di a_1
nuovo. 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 J
nostra join
funzione. 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.)
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]}
Se stai usando pandas
c'è una funzione nascosta in pandas.io.json._normalize
1 chiamata nested_to_record
che 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.x
e precedente pandas.io.json.normalize
(senza _
)
from pandas.io.json._normalize import nested_to_record
. Notare il trattino basso ( _
) prima normalize
.
0.25.x
, ho aggiornato la risposta. :)
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}
('hgf',2)
il 2 ° tasto nei test TypeError
+
operatore. Per qualsiasi altra cosa dovrai adattarti prefix + separator + k
alla chiamata della funzione appropriata per comporre gli oggetti.
{'a_b':{'c':1}, 'a':{'b_c':2}}
{'name': 'Steven', 'children': [{'name': 'Jessica', 'children': []}, {'name': 'George', 'children': []}]}
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.
lkey=''
nella definizione della funzione anziché quando si chiama la funzione. Vedi altre risposte al riguardo.
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}
reduce
sia fantastico nel caso in cui sia necessario ridurre i dizionari. Ho aggiornato la risposta. Ora dovrebbe apparire un po 'più pitonico.
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)
d
non è un dict
tipo 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()
.
items
? Sarei curioso di vederne uno.
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}
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.
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 dictionary
che vogliamo appiattire. Il accumulator
parametro è qui per supportare la ricorsione, che vedremo più avanti. Quindi, creiamo un'istanza accumulator
in 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_key
argomento sarà None
per 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 v
la chiave k
sia 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 k
modo che può costruire la chiave concatenata. Si noti la continue
dichiarazione. Vogliamo saltare la riga successiva, al di fuori del if
blocco, in modo che il dizionario nidificato non finisca nella accumulator
chiave 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 v
non sia un dizionario? Mettilo invariato all'interno del file accumulator
.
accumulator[k] = v
Una volta terminato, restituiamo semplicemente il accumulator
, lasciando dictionary
intatta 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.
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}
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)
type([])
per evitare una chiamata di funzione per ogni elemento di dict
.
isinstance(v, list)
invece
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.
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_json
funzione corrispondente .
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))
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
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
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'}
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}
type(i).__name__=='dict'
potrebbe essere sostituito con type(i) is dict
o forse anche meglio isinstance(d, dict)
(o Mapping
/ MutableMapping
).
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)}
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 .
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.
Preferisco sempre gli dict
oggetti di accesso tramite .items()
, quindi per i dadi appiattiti uso il seguente generatore ricorsivo flat_items(d)
. Se ti piace averlo di dict
nuovo, 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
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)
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}}
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.
Basta usare python-benedict
, è una sottoclasse dict che offre molte funzionalità, incluso un flatten
metodo. È possibile installarlo usando pip:pip install python-benedict
https://github.com/fabiocaccamo/python-benedict#flatten
from benedict import benedict
d = benedict(data)
f = d.flatten(separator='_')