Suponha que você tenha um dicionário como:
{'a': 1,
'c': {'a': 2,
'b': {'x': 5,
'y' : 10}},
'd': [1, 2, 3]}
Como você planificaria isso em algo como:
{'a': 1,
'c_a': 2,
'c_b_x': 5,
'c_b_y': 10,
'd': [1, 2, 3]}
Suponha que você tenha um dicionário como:
{'a': 1,
'c': {'a': 2,
'b': {'x': 5,
'y' : 10}},
'd': [1, 2, 3]}
Como você planificaria isso em algo como:
{'a': 1,
'c_a': 2,
'c_b_x': 5,
'c_b_y': 10,
'd': [1, 2, 3]}
Respostas:
Basicamente, da mesma maneira que você achataria uma lista aninhada, basta fazer o trabalho extra para iterar o ditado por chave / valor, criando novas chaves para o seu novo dicionário e criando o dicionário na etapa final.
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}
isinstancepor um try..exceptbloco, isso funcionará para qualquer mapeamento, mesmo que não seja derivado dict.
collections.MutableMappingtorná-lo mais genérico. Mas para Python <2.6, try..excepté provavelmente a melhor opção.
if isinstance(v, collections.MutableMapping):paraif v and isinstance(v, collections.MutableMapping):
new_key = parent_key + sep + k if parent_key else kassume que as chaves são sempre strings, caso contrário, ele aumentará TypeError: cannot concatenate 'str' and [other] objects. No entanto, você pode corrigir isso simplesmente coagindo kem string ( str(k)) ou concatenando chaves em uma tupla em vez de em uma string (tuplas também podem ser chaves de ditado).
Há duas grandes considerações que o pôster original precisa considerar:
{'a_b':{'c':1}, 'a':{'b_c':2}}resultaria em {'a_b_c':???}. A solução abaixo evita o problema retornando uma iterável de pares.joinedKey = '_'.join(*keys), isso custará O (N ^ 2) tempo de execução. No entanto, se você estiver disposto a dizer nextKey = previousKey+'_'+thisKey, você recebe tempo de O (N). A solução abaixo permite fazer as duas coisas (já que você pode simplesmente concatenar todas as chaves e depois processá-las).(O desempenho provavelmente não é um problema, mas vou explicar o segundo ponto, caso alguém se importe: na implementação disso, existem inúmeras opções perigosas. Se você fizer isso de forma recursiva e produzir e reproduzir novamente, ou qualquer coisa equivalente que toque nós mais de uma vez (o que é bastante fácil de executar acidentalmente), você está realizando um trabalho potencialmente O (N ^ 2) em vez de O (N) Isso ocorre porque talvez você esteja calculando uma chave ae a_1depois a_1_i... e, em seguida, calculando aentão a_1então a_1_ii..., mas na verdade você não deveria ter que calcular a_1novamente. Mesmo se você não estiver recalculando, refazê-lo (uma abordagem de 'nível por nível') é igualmente ruim. Um bom exemplo é pensar sobre o desempenho {1:{1:{1:{1:...(N times)...{1:SOME_LARGE_DICTIONARY_OF_SIZE_N}...}}}})
Abaixo está uma função que escrevi flattenDict(d, join=..., lift=...)que pode ser adaptada a muitos propósitos e pode fazer o que você deseja. Infelizmente, é bastante difícil criar uma versão lenta dessa função sem incorrer nas penalidades de desempenho acima (muitos componentes python como chain.from_iterable não são realmente eficientes, o que eu só percebi após testes extensivos de três versões diferentes desse código antes de decidir sobre este).
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
Para entender melhor o que está acontecendo, abaixo está um diagrama para aqueles que não estão familiarizados com reduce(à esquerda), também conhecido como "dobra à esquerda". Às vezes, é desenhado com um valor inicial no lugar de k0 (não faz parte da lista, passado para a função). Aqui Jestá a nossa joinfunção. Nós pré-processamos cada k n com 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
Na verdade, é o mesmo que functools.reduce, mas onde nossa função faz isso em todos os caminhos-chave da árvore.
>>> reduce(lambda a,b:(a,b), range(5))
((((0, 1), 2), 3), 4)
Demonstração (que eu colocaria em 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}
Atuação:
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
... suspiro, não pense que isso é culpa minha ...
[nota histórica sem importância devido a problemas de moderação]
Em relação à suposta duplicata de Flatten, um dicionário de dicionários (2 níveis de profundidade) de listas em Python :
A solução dessa pergunta pode ser implementada em termos desta, fazendo sorted( sum(flatten(...),[]) ). O inverso não é possível: embora seja verdade que os valores de flatten(...)podem ser recuperados da suposta duplicada mapeando um acumulador de ordem superior, não é possível recuperar as chaves. (editar: também acontece que a pergunta do suposto proprietário duplicado é completamente diferente, pois ela lida apenas com dicionários com exatamente 2 níveis de profundidade, embora uma das respostas nessa página dê uma solução geral.)
Ou, se você já estiver usando pandas, poderá fazê-lo da seguinte json_normalize()maneira:
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])
Resultado:
{'a': 1, 'c_a': 2, 'c_b_x': 5, 'c_b_y': 10, 'd': [1, 2, 3]}
Se você estiver usando, pandashá uma função oculta em pandas.io.json._normalize1 chamada nested_to_recordque faz exatamente isso.
from pandas.io.json._normalize import nested_to_record
flat = nested_to_record(my_dict, sep='_')
1 Nas versões pandas 0.24.xe uso mais antigo pandas.io.json.normalize(sem o _)
from pandas.io.json._normalize import nested_to_record. Observe o sublinhado ( _) antes normalize.
0.25.x, eu atualizei a resposta. :)
Aqui está um tipo de implementação "funcional" e "one-liner". É recursivo e baseado em uma expressão condicional e uma compreensão de ditado.
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 }
Teste:
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)para a 2ª chave em seu teste de lançaTypeError
+operador. Para qualquer outra coisa, você precisará se adaptar prefix + separator + kà chamada de função apropriada para compor os objetos.
{'a_b':{'c':1}, 'a':{'b_c':2}}
{'name': 'Steven', 'children': [{'name': 'Jessica', 'children': []}, {'name': 'George', 'children': []}]}
Código:
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,''))
Resultados:
$ python test.py
{'a': 1, 'c_a': 2, 'c_b_x': 5, 'd': [1, 2, 3], 'c_b_y': 10}
Estou usando python3.2, atualização para sua versão do python.
lkey=''em sua definição de função, em vez de chamar a função. Veja outras respostas a esse respeito.
Que tal uma solução funcional e de alto desempenho no 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(),
{}
))
Isso é ainda mais eficiente:
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(),
{}
))
Em 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ótimo, caso você precise reduzir dicionários. Eu atualizei a resposta. Agora deve parecer um pouco mais pitônico.
Isso não se restringe aos dicionários, mas a todos os tipos de mapeamento que implementam .items (). Além disso, é mais rápido, pois evita uma condição if. No entanto, os créditos vão para 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)
dnão for um dicttipo de mapeamento personalizado que não é implementado items, sua função falharia naquele momento. Portanto, ele não funciona para todos os tipos de mapeamento, mas apenas para os que implementam items().
items? Eu ficaria curioso para ver um.
Minha solução Python 3.3 usando geradores:
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}
Função simples para nivelar dicionários aninhados. Para Python 3, substitua .iteritems()por.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
A ideia / requisito era: obter dicionários simples sem manter as chaves dos pais.
Exemplo de uso:
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}
Manter as chaves dos pais também é simples.
Utilizando recursão, mantendo-o simples e legível por humanos:
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
A chamada é simples:
new_dict = flatten_dict(dictionary)
ou
new_dict = flatten_dict(dictionary, separator="_")
se quisermos mudar o separador padrão.
Um pequeno colapso:
Quando a função é chamada pela primeira vez, é chamada apenas passando o dictionaryque queremos achatar. O accumulatorparâmetro está aqui para dar suporte à recursão, que vemos mais adiante. Portanto, instanciamos accumulatorum dicionário vazio, onde colocaremos todos os valores aninhados do original dictionary.
if accumulator is None:
accumulator = {}
À medida que iteramos sobre os valores do dicionário, construímos uma chave para cada valor. O parent_keyargumento será Nonepara a primeira chamada, enquanto que para cada dicionário aninhado, ele conterá a chave apontando para ele, portanto, acrescentamos essa chave.
k = f"{parent_key}{separator}{k}" if parent_key else k
Caso o valor que va chave kestá apontando seja um dicionário, a função chama a si mesma, passando o dicionário aninhado, o accumulator(que é passado por referência, para que todas as alterações feitas nele sejam feitas na mesma instância) e a chave kpara que possamos pode construir a chave concatenada. Observe a continuedeclaração. Queremos pular a próxima linha, fora do ifbloco, para que o dicionário aninhado não acabe na accumulatorchave abaixo k.
if isinstance(v, dict):
flatten_dict(dict=v, accumulator=accumulator, parent_key=k)
continue
Então, o que fazemos caso o valor vnão seja um dicionário? Basta colocá-lo inalterado dentro do accumulator.
accumulator[k] = v
Quando terminamos, retornamos o accumulator, deixando o dictionaryargumento original intocado.
NOTA
Isso funcionará apenas com dicionários que tenham cadeias de caracteres como chaves. Ele funcionará com objetos hashable implementando o __repr__método, mas produzirá resultados indesejados.
Isso é semelhante à resposta de imran e ralu. Ele não usa um gerador, mas emprega recursão com um fechamento:
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}
_flatten_dictnunca é retornada, nem se espera que seja retornada. Talvez possa ser referido como uma subfunção ou uma função fechada .
A solução de Davoud é muito boa, mas não fornece resultados satisfatórios quando o dict aninhado também contém listas de dict, mas seu código pode ser adaptado para esse 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([])para evitar uma chamada de função para cada item do dict.
isinstance(v, list)vez disso
As respostas acima funcionam muito bem. Apenas pensei em adicionar a função unflatten que escrevi:
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: Isso não leva em conta '_' já presente nas chaves, assim como as contrapartidas achatadas.
Aqui está um algoritmo para substituição elegante e no local. Testado com Python 2.7 e Python 3.5. Usando o caractere de ponto como um separador.
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
Exemplo:
d = {'a': {'b': 'c'}}
flatten_json(d)
print(d)
unflatten_json(d)
print(d)
Resultado:
{'a.b': 'c'}
{'a': {'b': 'c'}}
Publiquei este código aqui junto com a unflatten_jsonfunção correspondente .
Se você deseja nivelar um dicionário aninhado e desejar uma lista de todas as chaves exclusivas, aqui está a solução:
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
Eu estava pensando em uma subclasse de UserDict para nivelar automaticamente as chaves.
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)
Advantages As vantagens são que as teclas podem ser adicionadas em tempo real, ou usando instanciação de ditado padrão, sem surpresa:
>>> 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'}
Usando geradores:
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'pode ser substituído por type(i) is dictou talvez até melhor isinstance(d, dict)(ou Mapping/ MutableMapping).
Usando dict.popitem () na recursão simples como uma lista aninhada:
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)}
Não é exatamente o que o OP pediu, mas muitas pessoas estão vindo aqui procurando maneiras de nivelar dados JSON aninhados no mundo real que podem ter objetos json e matrizes json com valor-chave e objetos json dentro das matrizes e assim por diante. O JSON não inclui tuplas, portanto não precisamos nos preocupar com elas.
Encontrei uma implementação do comentário de inclusão na lista de @roneo na resposta postada por @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)
Teste-o:
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}
E isso faz o trabalho que eu preciso: joguei qualquer json complicado nisso e isso o achatou para mim.
Todos os créditos em https://github.com/ScriptSmith .
Na verdade, eu escrevi recentemente um pacote chamado cherrypicker para lidar com esse tipo exato de coisa, já que eu tinha que fazer isso com tanta frequência!
Acho que o código a seguir daria exatamente o que você procura:
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()
Você pode instalar o pacote com:
pip install cherrypicker
... e há mais documentos e orientações em https://cherrypicker.readthedocs.io .
Outros métodos podem ser mais rápido, mas a prioridade deste pacote é fazer com que essas tarefas fáceis . Se você possui uma grande lista de objetos para achatar, também pode pedir ao CherryPicker para usar o processamento paralelo para acelerar as coisas.
Eu sempre prefiro acessar dictobjetos via .items(), então, para aplainar dictos, uso o seguinte gerador recursivo flat_items(d). Se você gostaria de ter dictnovamente, simplesmente envolva-o assim: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
Variação desses dicionários Flatten aninhados, compactando chaves com max_level e redutor personalizado.
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 você não se importa com funções recursivas, aqui está uma solução. Também tomei a liberdade de incluir um parâmetro de exclusão , caso haja um ou mais valores que você deseja manter.
Código:
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)
Resultado:
{'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}}
Tentei algumas das soluções nesta página - embora não todas -, mas as que tentei falharam ao lidar com a lista aninhada de dict.
Considere um ditado como este:
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': []
}
]
}
}
Aqui está minha solução improvisada:
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
que produz:
{
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,
}
Uma solução improvisada e não é perfeita.
NOTA:
não mantém ditados vazios, como o address: {}par k / v.
não achatará os dicionários nas tuplas aninhadas - embora seja fácil adicionar usando o fato de que as tuplas python agem de maneira semelhante às listas.
Basta usar python-benedict, é uma subclasse dict que oferece muitos recursos, incluindo um flattenmétodo. É possível instalá-lo usando o pip:pip install python-benedict
https://github.com/fabiocaccamo/python-benedict#flatten
from benedict import benedict
d = benedict(data)
f = d.flatten(separator='_')