Achatar dicionários aninhados, compactar chaves

172

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]}
A Timmes
fonte
2
Além disso, há uma biblioteca para ele: github.com/ianlini/flatten-dict
Ufos
veja também: stackoverflow.com/questions/14692690
dreftymac 18/01/19

Respostas:

220

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}
Imran
fonte
7
Se você substituir o isinstancepor um try..exceptbloco, isso funcionará para qualquer mapeamento, mesmo que não seja derivado dict.
Björn Pollex
1
Alterado para teste para collections.MutableMappingtorná-lo mais genérico. Mas para Python <2.6, try..excepté provavelmente a melhor opção.
Imran
5
Se você quiser dicionários vazios preservados na versão achatada que você pode querer mudar if isinstance(v, collections.MutableMapping):paraif v and isinstance(v, collections.MutableMapping):
tarequeh
3
Note que 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).
21715 Scott Scott H
1
E a função de inflar é aqui
Mitch
65

Há duas grandes considerações que o pôster original precisa considerar:

  1. Existem problemas de espaço no teclado? Por exemplo, {'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.
  2. Se o desempenho é um problema, a função de redutor de chave (que aqui me refiro como 'junção') requer acesso a todo o caminho da chave, ou ele pode simplesmente funcionar com O (1) em todos os nós da árvore? Se você quiser dizer 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.)

ninjagecko
fonte
2
Não tenho certeza se isso é relevante para a questão. Esta solução não achata um item de dicionário de uma lista de dicionários, ou seja, {'a': [{'aa': 1}, {'ab': 2}]}. A função flattenDict pode ser alterada facilmente para acomodar este caso.
Stewbaca 02/03
55

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]}
MYGz
fonte
4
ou apenas passar o argumento de setembro :)
Blue Moon
2
Uma pena ele não controla listas :)
Roelant
31

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

Aaron N. Brock
fonte
1
O que funcionou para mim foi from pandas.io.json._normalize import nested_to_record. Observe o sublinhado ( _) antes normalize.
Eyal Levin
2
@EyalLevin Good catch! Isso mudou 0.25.x, eu atualizei a resposta. :)
Aaron N. Brock
28

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}
divida por zero
fonte
Isso não funciona para dicionários gerais, especificamente, com as chaves tupla, por exemplo substituto ('hgf',2)para a 2ª chave em seu teste de lançaTypeError
alancalvitti
@alancalvitti Isso pressupõe que seja uma string ou algo mais que suporte o +operador. Para qualquer outra coisa, você precisará se adaptar prefix + separator + kà chamada de função apropriada para compor os objetos.
dividebyzero
Outra questão relevante para as chaves da tupla. Publiquei separadamente como generalizar com base no seu método. No entanto, ele não pode lidar corretamente com o exemplo de ninjageko:{'a_b':{'c':1}, 'a':{'b_c':2}}
alancalvitti 05/07/19
2
Eu estava ficando preocupado, não vendo respostas utilizando recursão. O que há de errado com a nossa juventude nos dias de hoje?
Jakov
não faz nada se um ditado aninhar uma lista de ditados, assim:{'name': 'Steven', 'children': [{'name': 'Jessica', 'children': []}, {'name': 'George', 'children': []}]}
Gergely M
12

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.

Pavan Yalamanchili
fonte
Você provavelmente deseja especificar o valor padrão lkey=''em sua definição de função, em vez de chamar a função. Veja outras respostas a esse respeito.
Acumenus
6

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}
Rotareti
fonte
2
Que tal uma solução legível e funcional? ;) Em qual versão você testou isso? Estou recebendo "Erro de sintaxe" ao tentar fazer isso no Python 3.4.3. Parece que o uso de "** all" não é legítimo.
Ingo Fischer
Eu trabalho desde o Python 3.5. Não sabia que não funciona com o 3.4. Você está certo, isso não é muito legível. Eu atualizei a resposta. Espero que seja mais legível agora. :)
Rotareti
1
Adicionado falta reduzir importação. Ainda acho o código difícil de entender e acho que é um bom exemplo porque o próprio Guido van Rossum já desencorajou o uso de lambda, reduza, filtre e mapeie em 2005: artima.com/weblogs/viewpost.jsp?thread=98196
Ingo Fischer
Concordo. O Python não é realmente projetado para programação funcional . Ainda acho reduceótimo, caso você precise reduzir dicionários. Eu atualizei a resposta. Agora deve parecer um pouco mais pitônico.
Rotareti
6

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)
Davoud Taghawi-Nejad
fonte
1
Se 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().
user6037143
@ user6037143 você já encontrou um tipo de mapeamento que não implementa items? Eu ficaria curioso para ver um.
Trey Hunner
1
@ user6037143, não, por definição, se os itens não forem implementados, não haverá tipo de mapeamento.
Davoud Taghawi-Nejad 18/04/19
@ DavoudTaghawi-Nejad, você poderia modificar isso para lidar com chaves gerais, por exemplo, tuplas que não devem ser achatadas internamente.
Alancalvitti 03/07/19
5

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}
Atul
fonte
você pode estender para lidar com qualquer tipo de chave válido que não seja str (incluindo tupla)? Em vez de concatenação de cadeias, junte-as a uma tupla.
alancalvitti
4

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.

Ivy Growing
fonte
4

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.

Jakov
fonte
3

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}
Jonathan Drake
fonte
Não tenho certeza se o uso do termo " encerramento " está correto aqui, pois a função _flatten_dictnunca é retornada, nem se espera que seja retornada. Talvez possa ser referido como uma subfunção ou uma função fechada .
Acumenos
3

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)
user3830731
fonte
Você pode armazenar em cache o resultado type([])para evitar uma chamada de função para cada item do dict.
bfontaine
2
Por favor, use em isinstance(v, list)vez disso
Druska
2

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.

tarequeh
fonte
2

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 .

Alexander Ryzhov
fonte
2

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))
Ranvijay Sachan
fonte
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
fonte
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
fonte
isso funciona com listas dentro de nosso dict aninhado, mas não tem uma opção de separador personalizado
Nikhil VJ
2

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'}
Loutre
fonte
1
Atribuir a fd ['pessoa'], mas manter seu valor existente é bastante surpreendente. Não é assim que ditados regulares funcionam.
tbm
1

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}
Luka Rahne
fonte
2
type(i).__name__=='dict'pode ser substituído por type(i) is dictou talvez até melhor isinstance(d, dict)(ou Mapping/ MutableMapping).
Cristian Ciupitu
1

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)}
FredAKA
fonte
1

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 .

Nikhil VJ
fonte
1

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.

big-o
fonte
Eu gosto da abordagem alternativa.
Gergely M
0

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
Vladimir Ignatyev
fonte
0

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)
user2528473
fonte
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}}
Thomas
fonte
0

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.

Gergely M
fonte
-1

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='_')
Fabio Caccamo
fonte